Thread 내부의 값, 값을 갖고 있는 thread safe 기법을 적용할 수 있도록 도와주는 것이 ThreadLocal 이다.
이 ThreadLocal에 대해 살짝 파헤쳐보고자 한다.
호출하는 thread마다 다른 값을 사용할 수 있도록 get(), set() 함수가 있다.
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();
}
Java
복사
ThreadLocal 의 get 함수
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
Java
복사
ThreadLocal의 set 함수
위 함수들을 확인해 보면 Thread.currentThread() 함수를 통해서 현재 실행중인 thread에 접근하는 Thread의 기능이 있다.
Thread.currentThread()의 JNI 구현
Thread.currentThread() 함수를 통해서 현재 실행중인 thread에 접근하는 JNI 코드를 확인해보자.
코드는 openJDK에서 확인할 수 있다.
/**
* Returns a reference to the currently executing thread object. * * @return the currently executing thread.
*/@IntrinsicCandidate
public static native Thread currentThread();
Java
복사
이 기능은 JNI에서 제공한다. 매우 저수준의 기능으로 볼 수 있다.
// Method entry for java.lang.Thread.currentThread
address TemplateInterpreterGenerator::generate_currentThread() {
address entry_point = __ pc();
__ ldr(r0, Address(rthread, JavaThread::vthread_offset()));
__ resolve_oop_handle(r0, rscratch1, rscratch2);
__ ret(lr);
return entry_point;
}
C++
복사
JDK를 열어보면 이러한 코드가 있다.
상세히 살펴보자.
address entry_point = __ pc();
C++
복사
method의 시작점을 나타내는 주소를 가져온다.
현재 program counter의 값을 반환하며 기계어 코드의 시작 주소를 반환한다.
__ ldr(r0, Address(rthread, JavaThread::vthread_offset()));
C++
복사
ldr은 Load Register의 약제로, 메모리에서 값을 불러와 레지스터에 저장하는 ARM 어셈블리 명령어 이다.
이 명령어는 JavaThread::vthread_offset() 에서 지정된 오프셋의 메모리 값을 r0 레지스터에 로드한다.
__ resolve_oop_handle(r0, rscratch1, rscratch2);
C++
복사
r0 에 저장된 핸들을 resolve 한다.
JVM 내부에서 객체 참조를 안전하게 처리하기 위한 cpp의 매커니즘이다.
__ ret(lr);
C++
복사
ret는 return의 양자로 lr(link register)에 저장된 주소로 반환하는 ARM 어셈블리 명령어 이다.
이는 generate_currentThread 메서드의 실행을 종료하고 호출자에게 제어를 반환한다.
return entry_point;
C++
복사
메서드는 처음에 저장된 entry_point 주소를 반환하여 생성된 기계어 코드의 시작점을 나타낸다.
이러한 코드는 Java method의 호출을 빠르게 처리하기 위해 사용된다.
Singleton과 ThreadLocal
ThreadLocal 변수이러한 상황일 때 사용한다.
•
변경 가능한 싱글턴
•
전역 변수 등을 기반으로 설계되어 있는 구조에서 변수가 임의로 공유되는 상황을 막기 위해 사용하는 경우가 많다.
ObjectMapper 를 thread 마다 하나씩 생성한다고 가정해보며 아래와 같이 구성할 수 있다.
object ObjectMapperHolder {
private val threadLocal = ThreadLocal.withInitial { ObjectMapper() }
val mapper: ObjectMapper
get() = threadLocal.get()
}
Kotlin
복사
ThreadLocal의 내부 구현
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();
}
Java
복사
ThreadLocal 의 get 함수
이 함수에서 보면 현재 thread를 찾아와 그 thread를 map으로 get 하는 기능이 있다.
즉, ThreadLocal 은 내부적으로 Map<Thread, T> 로 구성된 것이다.
물론 실제 Map 은 아니고, ThreadLocalMap 이라하는 customized map을 사용한다.
/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
* */
static class ThreadLocalMap {
Java
복사
ThreadLocalMap은 ThreadLocal 안에 static inner class로 정의되어 있다.
Thread 별 값은 실제로 Thread 객체 자체에 저장되어 있으며, thread가 종료되면 값으로 할당된 부분도 GC되어 삭제된다.
이렇게 편리하긴 하지만, 전역 변수가 아니면서도 전역 변수처럼 동작하기 때문에 결합도가 증가하며 성능 오버헤드가 발생하는 등 자세히 학습하고 사용해야 한다.