4 min read

<Java> ThreadLocal 딥다이브

자바 병렬 프로그래밍을 읽으면서 문득 ThreadLocal이라는 클래스가 어떻게 스레드마다 서로 다른 참조를 유지시키는지 궁금해졌다. 이를 파악해본다. 먼저 공부를 하기 전 내가 모르는 사항은 아래와 같았다.

  1. ThreadLocal 도 클래스이고 인스턴스가 생성될 경우 힙 영역에 저장될 텐데, 어떻게 스레드의 로컬 영역에서 스레드마다의 별도로 존재하는 레퍼런스를 참조하는가?
  2. 스레드의 로컬 영역이라 함은 보통 스택영역을 말하는데 이 부분의 메모리를 사용하는건가?

우선 내부 구현을 먼저 살펴보았다. 가장 대표적인 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();
        }

스레드 로컬에 값을 저장하거나 가져가는 요청의 흐름을 정리해보며 다음과 같다.

  1. ThreadLocal 인스턴스 내부에서 현재 스레드 인스턴스의 ThreadLocalMap을 가져온다.
  2. 배열로 관리되는 K-V 쌍에서 요청이 들어온 ThreadLocal 객체의 value를 저장하거나 가져온다.
  3. Key 값의 경우 ThreadLocal 인스턴스의 해시코드를 사용한다.
  4. ThreadLocal 클래스에 찾은 값을 넘겨주며 스레드 각각이 서로 다른 참조를 유지할 수 있게 한다.

위 로직을 시각화한 그림은 아래와 같다.

https://junhyunny.github.io/java/thread-local-class-in-java/
https://junhyunny.github.io/java/thread-local-class-in-java/

마지막으로 처음 내가 가졌던 의문들은 아래와 같이 해결된다.

ThreadLocal 도 클래스이고 인스턴스가 생성될 경우 힙 영역에 저장될 텐데, 어떻게 스레드의 로컬 영역에서 스레드마다의 별도로 존재하는 레퍼런스를 참조하는가?

-> Thread 내부의 ThreadLocalMap이라는 클래스를 통해서 해당 스레드가 가지는ThreadLocal에 대한 별도의 참조를 유지한다. 그리고 ThreadLocalmap은 ThreadLocal과 해당 타입의 Value을 K-V Entry로 유지시킨다. 그래서 스레드마다 서로 다른 각각의 스레드로컬 Value를 유지시킬 수 있다.

스레드의 로컬 영역이라 함은 보통 스택영역을 말하는데 이 부분의 메모리를 사용하는건가?

-> 그렇진 않다. ThreadLocalmap 또한 클래스이고 이 또한 인스턴스로 생성되기에 아마 힙 영역에 메모리가 유지될 것이다. 하지만 해당 인스턴스에 대한 참조를 스레드마다 각각 가지고 있기에 이를 이용해서 스레드마다 서로 다른 K-V 쌍인 엔트리를 찾아오고 저장할 수 있다.