개요
Java에서 ThreadLocal을 사용할 때 보통 아래와 같이 new 연산자를 통해 객체를 생성합니다.
private static final ThreadLocal<UUID> THREAD_LOCAL = new ThreadLocal<>();
처음에는 ThreadLocal 객체가 데이터를 저장하는 저장소 역할을 한다고 생각했지만, 코드를 분석해본 결과 ThreadLocal은 저장소가 아니라 각 스레드가 가지고 있는 저장소의 key 역할을 수행한다는 것을 알게 되었습니다.
ThreadLocal의 set() 메서드 분석
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
현재 스레드 가져오기
- Thread.currentThread()를 호출하여 현재 실행 중인 스레드를 가져옴
현재 스레드의 ThreadLocalMap 가져오기
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
- Thread 객체의 threadLocals 필드를 반환하는데, 이 필드는 ThreadLocalMap 타입
/* 각 스레드가 가지고 있는 ThreadLocalMap */
ThreadLocal.ThreadLocalMap threadLocals = null;
- 즉, ThreadLocal이 직접 값을 저장하는 것이 아니라, 각 Thread 객체가 자체적으로 관리하는 ThreadLocalMap에 데이터를 저장
ThreadLocalMap 내부 자료구조
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}
- ThreadLocalMap 내부에는 Entry라는 내부 클래스를 가지고 있으며, 여기서 각 스레드에 할당된 ThreadLocal 데이터가 저장
set() 동작 과정
- getMap(t)를 호출하여 ThreadLocalMap이 존재하는지 확인
- 존재하면 map.set(this, value);를 호출하여 값을 저장
- 존재하지 않으면 createMap(t, value);를 호출하여 새로운 ThreadLocalMap을 생성
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- Thread 객체의 threadLocals 필드에 ThreadLocalMap 객체를 생성하고, 현재 ThreadLocal 인스턴스와 초기값을 저장
ThreadLocalMap의 구조
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
- table = new Entry[INITIAL_CAPACITY]; -> Entry[] 배열이 ThreadLocal의 키-값을 저장하는 공간
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
- firstKey.threadLocalHashCode는 ThreadLocal 객체의 해시 코드
- & (INITIAL_CAPACITY - 1) 비트 연산을 통해 해시 충돌을 최소화하여 해시인덱스를 생성
- i 는 해시 인덱스를 사용하여 O(1) 의 빠른성능을 보장
table[i] = new Entry(firstKey, firstValue);
- firstKey는 현재 ThreadLocal 객체의 참조값이며, firstValue는 저장하려는 값
- 해당 데이터를 Entry 객체로 생성하여 table에 저장
ThreadLocal의 get() 메서드 분석
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();
}
현재 스레드에서 ThreadLocalMap 가져오기
- getMap(Thread t)를 호출하여 현재 스레드의 ThreadLocalMap을 가져옴
현재 ThreadLocal의 값 찾기
- map.getEntry(this); → 현재 ThreadLocal 객체의 참조값(this)을 키로 하여 Entry를 찾음
- 만약 존재하면 값을 반환하고, 없으면 setInitialValue()를 호출하여 기본값을 설정
ThreadLocal의 remove() 메서드
- 스프링 프레임워크에서는 스레드 풀 환경, 즉 스레드 풀은 스레드를 반복해서 재사용 이전 요청에서 설정된 ThreadLocal 값을 삭제해줘야 함
public void remove() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.remove(this);
}
}
- remove()를 호출하면 현재 스레드의 ThreadLocalMap에서 해당 ThreadLocal 키를 삭제하여 불필요한 메모리 점유를 방지
결론
각 스레드는 자체적으로 ThreadLocalMap을 가지고 있기 때문에 충돌 없이 스레드 세이프함
ThreadLocalMap의 내부 자료구조인 Entry는 ThreadLocal 자신의 객체를 키로 사용하여 데이터를 저장
하나의 스레드에서 여러 개의 ThreadLocal 객체를 사용할 수 있으며, 각각의 데이터를 안전하게 관리할 수 있음
ThreadLocal 인스턴스
| ThreadLocal<T> @x001 |
ThreadLocal<T> @x002 |
Thread1.ThreadLocalMap
| ThreadLocal<T> | value |
| @x001 | data |
| @x002 | data2 |
Thread2.ThreadLocalMap
| ThreadLocal<T> | value |
| @x001 | data |
| @x002 | data2 |
즉, ThreadLocal은 데이터를 직접 저장하는 것이 아니라, 각 스레드마다 독립적인 저장소를 관리하는 key 역할을 수행하며, 이를 통해 멀티스레드 환경에서도 안전하게 데이터를 보관할 수 있습니다.
'Java' 카테고리의 다른 글
| ExecutorService의 이해 (0) | 2025.02.19 |
|---|---|
| Spring Boot 엑셀 업로드 기능 개발기 (1) | 2025.02.04 |