ThreadLocal 이란??

영석·2026년 10월 5일

Java

목록 보기
4/4
post-thumbnail

ThreadLocal

회사에서 일하면서 처음마주친 이녀석을 제 개인프로젝트에서 한번 사용해보고자 포스팅을 작성하였습니다.
팀 프로젝트 및 개인 학습을 병행하면서 ThreadLocal을 활용한적도, 하는것을 본적도 없던지라 저에겐 생소한 자바 클래스였는데요

잘 활용하면 기존 제가 느꼈던 불편함을 해소하면서 장점을 취할 수 있을것같아 정리해보겠습니다.

개념

ThreadLocal은 스레드마다 따로 값을 저장하고 꺼내게 해주는 객체다. 같은 ThreadLocal 객체 하나를 여러 스레드가 함께 써도, get()을 호출하면 각 스레드는 자기가 넣은 값만 돌려받는다.

비유를 하자면 Thread마다 가방을 메고 있는 느낌이 정확하다고 생각한다. 실제로 Spring의 트랜잭션 관리와 SecurityContextHolder도 이 방식으로 동작한다.

이제 실제 동작을 코드로 확인해보고 분석해보겠다!


ThreadLocal 실측 테스트

private static final ThreadLocal<Integer> tl = new ThreadLocal<>();

public static void main(String[] args) throws InterruptedException {

    tl.set(0); // main 스레드 - 스레드로컬 값 설정
    System.out.println(Thread.currentThread().getName() + " 시작 get() = " + tl.get());
		
		// Th-A, Th-B 스레드 실행
    Thread thA = new Thread(task(100), "Th-A");
    Thread thB = new Thread(task(200), "Th-B");
  
    thA.start();
    thB.start();
    thA.join();
    thB.join();
		
		// Main 스레드 - 스레드로컬 값 출력
    System.out.println(Thread.currentThread().getName() + " 끝 get() = " + tl.get());
}

private static Runnable task(int value) {
    return () -> {
        String name = Thread.currentThread().getName();
        System.out.println(name + " 시작 get() = " + tl.get());
        tl.set(value); // 스레드 로컬 값 설정

        System.out.println(name + " 종료 get() = " + tl.get());
        //tl.remove();
    };
}
  • Main 스레드의 값은 유지되고, A,B 값은 각각 다른 스레드이기 때문에 값이 다르게 출력되는것을 확인할 수 있다.

각 스레드마다 데이터를 독립적으로 관리 한다는 것을 확인하였다. 어떻게 구현되어있을까?


Deep Dive

--- ThreadLocal.java --------------------------------------------------------

public T get() {
    return get(Thread.currentThread());
}
  
private T get(Thread t) {
    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(t);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
-----------------------------------------------------------------------------


--- Thread.java -------------------------------------------------------------

/*
 * ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class.
 */
ThreadLocal.ThreadLocalMap threadLocals;

-----------------------------------------------------------------------------

아래와 같은 과정을 거쳐 get(); 메서드가 작동합니다.

  1. 인자없는 get() 호출 → Thread.currentThread()를 인자로 받는 오버로딩 메서드인 get(Thread t)에 호출 위임
  2. getMap()을 통해 Thread 내부 threadLocals 필드 접근 → ThreadLocalMap 반환
  3. getEntry(this) → 현재 ThreadLocal 인스턴스를 Key로 활용하여 저장된 값 조회

여기서 핵심은 각각의 Thread 클래스가 ThreadLocalMap을 관리한다. 하지만 의문점이 생겼다, 만약 내가 설계자라면 ThreadLocal에 ThreadLocalMap 전역 필드를 선언하고 thread기반 key조회를 하는 형식으로 개발했을 것 같기때문이다.

히스토리 확인 결과 JDK1.2에서 내 생각과 동일하게 개발이 되었고, JDK1.3에서는 현 상태로 구조가 변경됐다.


JDK 1.2는 ThreadLocal 객체마다 Map을 선언했다!

하지만 JDK1.3에서 이 구조를 뒤집어(ThreadLocal마다 맵 → Thread마다 맵) 지금까지 이어져 왔다. 히스토리를 보고 알 수 있는 의도는 다음과 같다. (https://bugs.openjdk.org/browse/JDK-4252910) 참고

  • Lock이 필요 없다: ThreadLocal마다 맵을 두면 모든 스레드가 같은 맵을 쓰므로 Lock이 필요하고, 이는 성능 손해다. Thread마다 맵을 두면 주인 스레드 혼자 쓰므로 Lock이 필요 없다
  • 메모리 누수 방지: 스레드를 key로 하는 맵을 ThreadLocal 쪽에 두면, 스레드가 죽어도 맵에 그 스레드의 값이 남는다. 하지만Thread가 맵을 가지면 그런 장치 없이도 스레드가 끝날 때 맵이 함께 정리된다. 단, 스레드풀처럼 스레드가 재사용 된다면, 값이 계속 남으므로 다 쓴 뒤 remove()가 필요하다.

추가로 덧붙이자면 하나의 스레드가 여러 ThreadLocal을 사용한다는 확장성 관점에서도 필요한 변경이였다고 생각이든다 ㅎㅎ;;

profile
느리게 갱신되는 개발실력 - >_0

0개의 댓글