<Java> ThreadLocal 딥다이브
자바 병렬 프로그래밍을 읽으면서 문득 ThreadLocal
이라는 클래스가 어떻게 스레드마다 서로 다른 참조를 유지시키는지 궁금해졌다. 이를 파악해본다. 먼저 공부를 하기 전 내가 모르는 사항은 아래와 같았다.
ThreadLocal
도 클래스이고 인스턴스가 생성될 경우 힙 영역에 저장될 텐데, 어떻게 스레드의 로컬 영역에서 스레드마다의 별도로 존재하는 레퍼런스를 참조하는가?- 스레드의 로컬 영역이라 함은 보통 스택영역을 말하는데 이 부분의 메모리를 사용하는건가?
우선 내부 구현을 먼저 살펴보았다. 가장 대표적인 get,set의 경우 아래와 같이 구현된다. 즉, 내부적으로 ThreadLocalMap
이라는 인스턴스를 활용하여 스레드마다의 참조를 유지시킨다. set 메서드를 살펴보면 <key-value>
쌍이 <ThreadLocal - T 타입의 Value>
으로 저장됨을 알 수 있다. 즉, ThreadLocal 클래스마다 이에 해당하는 값들을 각각 유지하고 있다.
public class ThreadLocal<T> {
//...
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
}
참고로 스레드 클래스의 구현 내부에 아래와 같이 threadLocals이라는 필드값으로 ThreadLocalMap
이 유지되고 있다. 즉, 스레드마다 각각 자신이 가진 ThreadLocal 상태를 유지시키는 자료구조를 활용한다.
public class Thread implements Runnable {
//...
ThreadLocal.ThreadLocalMap threadLocals = null;
}
ThreadLocalMap
에 대해서 좀 더 알아보면 내부적으로 배열을 통해 K-V 쌍의 관리한다. 아래는 ThreadLocalMap
의 set 메서드 구현이다. 간략히 살펴보면 Entry
타입의 배열 자료구조에 key로부터 얻은 인덱스 값을 통해 value를 할당시키며 값들을 관리한다.
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
스레드 로컬에 값을 저장하거나 가져가는 요청의 흐름을 정리해보며 다음과 같다.
- ThreadLocal 인스턴스 내부에서 현재 스레드 인스턴스의
ThreadLocalMap
을 가져온다. - 배열로 관리되는 K-V 쌍에서 요청이 들어온
ThreadLocal
객체의 value를 저장하거나 가져온다. - Key 값의 경우 ThreadLocal 인스턴스의 해시코드를 사용한다.
ThreadLocal
클래스에 찾은 값을 넘겨주며 스레드 각각이 서로 다른 참조를 유지할 수 있게 한다.
위 로직을 시각화한 그림은 아래와 같다.
마지막으로 처음 내가 가졌던 의문들은 아래와 같이 해결된다.
ThreadLocal
도 클래스이고 인스턴스가 생성될 경우 힙 영역에 저장될 텐데, 어떻게 스레드의 로컬 영역에서 스레드마다의 별도로 존재하는 레퍼런스를 참조하는가?
-> Thread 내부의 ThreadLocalMap
이라는 클래스를 통해서 해당 스레드가 가지는ThreadLocal
에 대한 별도의 참조를 유지한다. 그리고 ThreadLocalmap은 ThreadLocal
과 해당 타입의 Value을 K-V Entry로 유지시킨다. 그래서 스레드마다 서로 다른 각각의 스레드로컬 Value를 유지시킬 수 있다.
스레드의 로컬 영역이라 함은 보통 스택영역을 말하는데 이 부분의 메모리를 사용하는건가?
-> 그렇진 않다. ThreadLocalmap
또한 클래스이고 이 또한 인스턴스로 생성되기에 아마 힙 영역에 메모리가 유지될 것이다. 하지만 해당 인스턴스에 대한 참조를 스레드마다 각각 가지고 있기에 이를 이용해서 스레드마다 서로 다른 K-V 쌍인 엔트리를 찾아오고 저장할 수 있다.