Java 8 ThreadLocal 源碼解析

Java 中的 ThreadLocal是線程內的局部變量, 它爲每一個線程保存變量的一個副本。ThreadLocal 對象能夠在多個線程中共享, 但每一個線程只能讀寫其中本身的副本。java

目錄:dom

代碼示例

咱們編寫一個簡單的示例:ide

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author finley
 */
public class MyThread extends Thread {

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

    private static final Random random = new Random();

    @Override
    public void run() {
        int localValue = random.nextInt();
        threadLocal.set(localValue);
        System.out.println("Thread: " + Thread.currentThread().getName() + " set thread local: " + localValue);
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread: " + Thread.currentThread().getName() + " threadLocal: " + threadLocal.get() + ", local: " + localValue);
    }

    public static void main(String[] args) {
        int concurrent = 10;
        ExecutorService service = Executors.newFixedThreadPool(concurrent);
        for (int i = 0; i < concurrent; i++) {
            service.execute(new MyThread());
        }
        service.shutdown();
    }

}

運行結果:this

Thread: pool-1-thread-1 set thread local: -1262320606
Thread: pool-1-thread-2 set thread local: 1222545653
Thread: pool-1-thread-3 set thread local: 2067394038
Thread: pool-1-thread-4 set thread local: 920362206
Thread: pool-1-thread-5 set thread local: -1977931750
Thread: pool-1-thread-6 set thread local: 985735150
Thread: pool-1-thread-7 set thread local: -602752866
Thread: pool-1-thread-8 set thread local: 672437027
Thread: pool-1-thread-9 set thread local: 1063652674
Thread: pool-1-thread-10 set thread local: 1790288576
Thread: pool-1-thread-1 threadLocal: -1262320606, local: -1262320606
Thread: pool-1-thread-3 threadLocal: 2067394038, local: 2067394038
Thread: pool-1-thread-4 threadLocal: 920362206, local: 920362206
Thread: pool-1-thread-6 threadLocal: 985735150, local: 985735150
Thread: pool-1-thread-7 threadLocal: -602752866, local: -602752866
Thread: pool-1-thread-2 threadLocal: 1222545653, local: 1222545653
Thread: pool-1-thread-5 threadLocal: -1977931750, local: -1977931750
Thread: pool-1-thread-8 threadLocal: 672437027, local: 672437027
Thread: pool-1-thread-10 threadLocal: 1790288576, local: 1790288576
Thread: pool-1-thread-9 threadLocal: 1063652674, local: 1063652674

能夠看到10個線程調用同一個ThreadLocal對象的set方法寫入隨機值, 而後讀取到本身寫入的副本。線程

源碼解析

咱們從ThreadLocal.set方法開始分析:code

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

set方法將當前線程的副本寫入了一個ThreadLocalMap, map的key是當前的ThreadLocal對象。對象

接下來經過getMap方法分析這個ThreadLocalMap是如何維護的:繼承

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

public
class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

每一個 Thread 對象維護了一個 ThreadLocalMap 類型的 threadLocals 字段。內存

ThreadLocalMap 的 key 是 ThreadLocal 對象, 值則是變量的副本, 所以容許一個線程綁定多個 ThreadLocal 對象ci

理解副本的管理機制後很容易理解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;
}

首先得到當前線程的ThreadLocalMap, 而後從 ThreadLocalMap 嘗試得到當前 ThreadLocal 對象對應的副本。

若獲取失敗,則寫入並返回initialValue方法定義的默認值。

Thread.threadLocals 字段是惰性初始化的。 ThreadLocal.set() 方法發現 threadLocals 爲空時會調用 createMap 方法進行初始化, ThreadLocal.get()方法一樣會在setInitialValue() 中調用 createMap 方法初始化 Thread.threadLocals 字段。

爲了避免影響讀者總體瞭解ThreadLocal, ThreadLocalMap 的實現原理在最後一節ThreadLocalMap

InheritableThreadLocal

InheritableThreadLocal 在子線程建立時將父線程的變量副本傳遞給子線程。

InheritableThreadLocal 繼承了 ThreadLocal 並重寫了3個方法, 它使用 Thread.inheritableThreadLocals 代替了 Thread.threadLocals 字段。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

ThreadLocalMap 的構造器中實現了向子線程傳遞的邏輯:

private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

Thread.init 方法調用此構造器傳遞 InheritableThreadLocal:

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

ThreadLocalMap

值得一提的是, ThreadLocalMap 中使用的是 WeakReference, 當 ThreadLocal 對象再也不被外部引用時, 弱引用不會阻止GC所以避免了內存泄露

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;
            }
        }

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0
}

Entry 的 key 始終是 ThreadLocal 對象, 值則是 ThreadLocal 對象綁定的變量副本。

Get 流程

首先來看 ThreadLocalMap.getEntry 方法:

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);
}

利用 table 大小始終爲2的整數冪的特色使用位運算找到哈希槽。

若哈希槽中爲空或 key 不是當前 ThreadLocal 對象則會調用getEntryAfterMiss方法:

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;
}

ThreadLocalMap 使用開放定址法處理哈希衝突, nextIndex 方法會提供哈希衝突時下一個哈希槽的位置。

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

nextIndex 返回下一個位置, 到達末尾後返回第一個位置0.

getEntryAfterMiss 方法會循環查找直到找到或遍歷全部可能的哈希槽, 在循環過程當中可能遇到4種狀況:

  • 哈希槽中是當前ThreadLocal, 說明找到了目標
  • 哈希槽中爲其它ThreadLocal, 須要繼續查找
  • 哈希槽中爲null, 說明搜索結束未找到目標
  • 哈希槽中存在Entry, 可是 Entry 中沒有 ThreadLocal 對象。由於 Entry 使用弱引用, 這種狀況說明 ThreadLocal 被GC回收。

爲了處理GC形成的空洞(stale entry), 須要調用expungeStaleEntry方法進行清理。

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 清理當前的空洞
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
            (e = tab[i]) != null;
            i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) { 
            // 若下一個位置仍是空洞將其一併清除
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 發現不是空洞的 Entry 將其放入最靠前的哈希槽中
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;
                while (tab[h] != null) // 處理移動過程當中的哈希衝突
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
        // 循環執行直到遇到空的哈希槽, 代表從 staleSlot 開始的查找序列中間不會存在空哈希槽或空Entry
    }
    return i;
}

清理分爲兩個部分:

  1. 首先清理掉空的Entry
  2. Entry被清理後可能會使 getEntryAfterMiss 方法誤覺得搜索已經結束,所以須要將後面的 Entry 進行 rehash 填補空洞

在執行清理時, 可能由於GC形成多個空洞所以須要循環清理。

Set 流程

首先來看 ThreadLocalMap.set 方法:

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    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();
}

首先計算哈希槽的位置, 此時可能有3種狀況:

  1. 哈希槽爲空, 直接將新 Entry 填入槽中; 此外調用 cleanSomeSlots 搜索並清理 GC 形成的空洞; 此外檢查 Entry 數量是否到達閾值, 必要時調用 rehash 方法進行擴容。
  2. 哈希槽中爲當前 ThreadLocal, 直接進行替換
  3. 哈希槽中爲空 Entry, 說明原有ThreadLocal 被 GC 回收, 調用 replaceStaleEntry 將其替換。

接下來重點分析 replaceStaleEntry:

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
            (e = tab[i]) != null;
            i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    for (int i = nextIndex(staleSlot, len);
            (e = tab[i]) != null;
            i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == key) {
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

replaceStaleEntry 方法看上去很是複雜, 簡單的說分爲三部分:

  1. 向前尋找空的 Entry 將其位置寫入 slotToExpunge, 這是爲了清理沒必要繼續關注
  2. 向後進行尋找如果找到與傳入的 key 相同 Entry 則更新 Entry 的內容並將其移動到 staleSlot, 而後調用 cleanSomeSlots 進行清理
  3. 若最終沒有找到 key 相同的Entry, 則在 staleSlot 處寫入一個新的 Entry, 調用 cleanSomeSlots 進行清理

cleanSomeSlots 調用 expungeStaleEntry 從位置 i 開始向後清理。

執行log2(n)次清理以取得清理效果(剩餘空洞數量)和清理耗時之間的平衡。

private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

簡單看一下 rehash 的過程:

private void rehash() {
    expungeStaleEntries();

    if (size >= threshold - threshold / 4)
        resize();
}

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}

首先進行清理,若清理後sz > thresholde * 0.75將哈希表的的大小翻倍。

Remove

remove 方法和 get 方法比較相似:

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;
        }
    }
}
相關文章
相關標籤/搜索