원본에서 ThreadLocal의 밑바닥 구조를 보다
13540 단어 Java 소스 분석
ThreadLocal
는 다중 스레드 상황에서의 변수 보존에 사용할 수 있으며 각 스레드 간의 변수 값은 서로 영향을 주지 않는다.그 사용 장면에 대해 유흔 선생님의 글을 보면 하얀 라인의 개인 영지인 Thread Local을 설명할 수 있다. 이 글은 생동감 있는 배열을 통해 사용 장면을 끌어내서 인상적이다.사용법
ThreadLocal threadLocalA= new ThreadLocal();
ThreadLocal threadLocalB = new ThreadLocal();
//
1: threadLocalA.set("Sbingo");
threadLocalB.set(99);
2: threadLocalA.set("sbingo");
threadLocalB.set(100);
//
1: threadLocalA.get() --> "Sbingo"
threadLocalB.get() --> "99"
2: threadLocalA.get() --> "sbingo"
threadLocalB.get() --> "100"
보시다시피
ThreadLocal
의 사용 방법은 매우 간단합니다. 왜 이렇게 조작한 후에 같은 변수가 서로 다른 라인에서 꺼낸 값이 다르고 1개의 라인은 N개ThreadLocal
를 저장할 수 있습니까?원리는
Thread
류에서 구성원 변수threadLocals
가 있는데 그 유형은 ThreadLocal.ThreadLocalMap
이다. 각 라인마다 이런 Map
가 있기 때문에 N개ThreadLocal
의 키 값이 맞고 서로 다른 라인의 변수 값이 다르다.따라서 위에서 설명한 대로 해당하는 데이터 구조는 다음과 같습니다.
스레드 1의 맵
key
value
threadLocalA
Sbingo
threadLocalB
99
스레드 2의 맵
key
value
threadLocalA
sbingo
threadLocalB
100
변수
threadLocals
는 Thread
클래스에 정의되어 있지만 그것에 대한 접근은 클래스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);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
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);
}
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
private void set(ThreadLocal> key, Object value) {
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();
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
11줄에서 알 수 있듯이
ThreadLocal.ThreadLocalMap
방법은 방금 언급한getMap()
류의 구성원 변수Thread
를 되돌려주는 것이다.처음
threadLocals
방법을 호출할 때 7행set()
방법에 들어가고 15행은 언급한 라인 변수createMap()
에 값을 부여한다. 이로부터 이것은 더 이상threadLocals
이 아니다.15행은 구조 방법
null
에 전입되었다. 즉, 현재의 this
자체를 ThreadLocal
로 삼았다.구조 방법에 들어가면
key
table
의 해시표이고 19줄은 그에 대한 값을 부여하며 초기 용량은 16이다.Map
에 저장된 원소의 유형은 table
로 Entry
에 대한 약한 인용(26줄)으로 메모리 유출을 방지한다.ThreadLocal
중 변수GC
(28행)만 있고 Entry
는 value
중 각value
에 대응하는 값이다.20행은
Map
과ThreadLocal
를 비트와 연산하여 threadLocalHashCode
대INITIAL_CAPACITY - 1
에 해당하지만 속도가threadLocalHashCode
로 연산하는 것보다 빨라서 해시표의 인덱스 위치를 계산했다.마지막으로 해시표에 해당하는 인덱스의 원소에 값을 부여하고 표 안의 원소 수량과 궐값을 업데이트하여 처음
INITIAL_CAPACITY
을 완성했다.이후
%
방법을 다시 호출하면 5행 코드에 들어간다.아니면 이전과 같이 색인을 계산해 충돌이 발생하면 41행에서 55행의 순환체에 들어간다.
64행에서 알 수 있듯이 충돌을 해결하는
set
방법은 선형 탐지법을 사용한다.46 ~ 49 줄, 같은
set()
대상이면 대응하는 값을 업데이트합니다.51~54줄에서
nextIndex()
의 인용이 ThreadLocal
이라면 현재의 새 값으로 옛 값을 대체하고 편폭을 간소화하기 위해 구체적인 원본 코드는 여기에서 펼치지 않습니다.충돌이 발생하지 않았거나 충돌이 발생하지 않았지만 반복이 끝났을 때 같은
ThreadLocal
대상이나 null
대상을 찾지 못하면 새 값을 삽입하고 57번째 줄을 실행합니다.59행은 새 값을 삽입한 후 회수한 값을 삭제하려고 시도한 것을 나타낸다. 삭제가 발생하지 않았고 해시 테이블 안의 원소 수량이 궐수 값을 초과하면 해시 테이블을 확장하고 목록 안의 원소를 다시 배열한다.
76행은 궐치 크기가 해시표 길이의 2/3이고 71행은 확장할 때 해시표 안의 원소 수량이 궐치의 3/4이며 곱하기는 해시표 크기의 1/2이다.즉 해시표 안의 원소 수량이 해시표 크기의 절반을 초과하면 확장이 발생한다.확장된 크기는 원 크기의 두 배로 여기서도 분석하지 않겠다.
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();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
private Entry getEntry(ThreadLocal> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
ThreadLocal
가 null
발생하기 전에 12행 코드에 들어갑니다.이후 16줄에서 호출get()
방법이 되돌아왔고set()
기본값을 수정하기 위해 이 방법을 다시 쓸 수도 있다.22 길드 초기화initialValue()
, 마지막으로 초기값null
이나 수정된 초기값)을 되돌려줍니다.물론 대다수 상황에서
Map
방법을 사용하면 5행에서 null
대상을 얻고 8, 9행에서 대응하는 get()
값을 얻으며 되돌아온다.이를 통해 알 수 있듯이 관건은 Entry
방법에 있다.31행은 색인을 계산하고 33행은 색인에 있는
value
의 대상이 getEntry()
인지 판단하고 두 개의 Entry
대상이 동일한지 비교한 결과 진짜가 되면 바로 null
대상으로 되돌아갈 수 있고 그렇지 않으면 ThreadLocal
방법으로 들어갈 수 있다.43행에서 52행의 순환체에서 해시표를 끊임없이 훑어보고 두 개의
Entry
대상이 같을 때 해시표의 현재 색인에 있는 getEntryAfterMiss()
대상을 되돌릴 수 있다.메모리 누설
ThreadLocal
사용하기는 간단하지만 사용 시 메모리 유출에 주의해야 한다.방금 분석할 때 말했듯이
Entry
대상은 ThreadLocal
의 약한 인용이다.그래서 라인이 사라질 때 쓰레기 수거기는 그것을 회수하는데 이때 메모리 유출 문제가 존재하지 않는다.그러나 스레드 탱크를 사용하고 스레드가 복용되는 상황이 존재하면 스레드가 사라지지 않고
Entry
대상이 누출될 수 있으므로 수동으로 정리해야 한다.청소하는 방법은 간단하다.
ThreadLocal
의 Entry
방법을 호출하는 것이다. 위의 분석과 유사하게 색인을 끊임없이 탐지하고 정확한 ThreadLocal
대상을 찾으면 청소한다.관련 소스는 다음과 같습니다.
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
private void remove(ThreadLocal> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}