원본에서 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
변수threadLocalsThread 클래스에 정의되어 있지만 그것에 대한 접근은 클래스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로 삼았다.
구조 방법에 들어가면keytable의 해시표이고 19줄은 그에 대한 값을 부여하며 초기 용량은 16이다.Map에 저장된 원소의 유형은 tableEntry에 대한 약한 인용(26줄)으로 메모리 유출을 방지한다.ThreadLocal 중 변수GC(28행)만 있고 Entryvalue 중 각value에 대응하는 값이다.
20행은 MapThreadLocal를 비트와 연산하여 threadLocalHashCodeINITIAL_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;
}

ThreadLocalnull 발생하기 전에 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 대상이 누출될 수 있으므로 수동으로 정리해야 한다.
청소하는 방법은 간단하다. ThreadLocalEntry 방법을 호출하는 것이다. 위의 분석과 유사하게 색인을 끊임없이 탐지하고 정확한 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;
        }
    }
}

좋은 웹페이지 즐겨찾기