Java 之 ThreadLocal 詳解

zly394發表於2017-07-12

1. 概念

ThreadLocal 用於提供執行緒區域性變數,在多執行緒環境可以保證各個執行緒裡的變數獨立於其它執行緒裡的變數。也就是說 ThreadLocal 可以為每個執行緒建立一個【單獨的變數副本】,相當於執行緒的 private static 型別變數。

ThreadLocal 的作用和同步機制有些相反:同步機制是為了保證多執行緒環境下資料的一致性;而 ThreadLocal 是保證了多執行緒環境下資料的獨立性。

2. 使用示例

public class ThreadLocalTest {
    private static String strLabel;
    private static ThreadLocal<String> threadLabel = new ThreadLocal<>();

    public static void main(String... args) {
        strLabel = "main";
        threadLabel.set("main");

        Thread thread = new Thread() {

            @Override
            public void run() {
                super.run();
                strLabel = "child";
                threadLabel.set("child");
            }

        };

        thread.start();
        try {
            // 保證執行緒執行完畢
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("strLabel = " + strLabel);
        System.out.println("threadLabel = " + threadLabel.get());
    }
}複製程式碼

執行結果:

strLabel = child
threadLabel = main複製程式碼

從執行結果可以看出,對於 ThreadLocal 型別的變數,在一個執行緒中設定值,不影響其在其它執行緒中的值。也就是說 ThreadLocal 型別的變數的值在每個執行緒中是獨立的。

3. ThreadLocal 實現

ThreadLocal 是怎樣保證其值在各個執行緒中是獨立的呢?下面分析下 ThreadLocal 的實現。

ThreadLocal 是建構函式只是一個簡單的無參建構函式,並且沒有任何實現。

3.1 set(T value) 方法

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(T value) 方法中,首先獲取當前執行緒,然後在獲取到當前執行緒的 ThreadLocalMap,如果 ThreadLocalMap 不為 null,則將 value 儲存到 ThreadLocalMap 中,並用當前 ThreadLocal 作為 key;否則建立一個 ThreadLocalMap 並給到當前執行緒,然後儲存 value。

ThreadLocalMap 相當於一個 HashMap,是真正儲存值的地方。

3.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();
}複製程式碼

同樣的,在 get() 方法中也會獲取到當前執行緒的 ThreadLocalMap,如果 ThreadLocalMap 不為 null,則把獲取 key 為當前 ThreadLocal 的值;否則呼叫 setInitialValue() 方法返回初始值,並儲存到新建立的 ThreadLocalMap 中。

3.3 initialValue() 方法:

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;
}
...複製程式碼

initialValue() 是 ThreadLocal 的初始值,預設返回 null,子類可以重寫改方法,用於設定 ThreadLocal 的初始值。

3.4 remove() 方法

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}複製程式碼

ThreadLocal 還有一個 remove() 方法,用來移除當前 ThreadLocal 對應的值。同樣也是同過當前執行緒的 ThreadLocalMap 來移除相應的值。

3.5 當前執行緒的 ThreadLocalMap

在 set,get,initialValue 和 remove 方法中都會獲取到當前執行緒,然後通過當前執行緒獲取到 ThreadLocalMap,如果 ThreadLocalMap 為 null,則會建立一個 ThreadLocalMap,並給到當前執行緒。

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

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
...複製程式碼

可以看到,每一個執行緒都會持有有一個 ThreadLocalMap,用來維護執行緒本地的值:

public class Thread implements Runnable {
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ...
}複製程式碼

在使用 ThreadLocal 型別變數進行相關操作時,都會通過當前執行緒獲取到 ThreadLocalMap 來完成操作。每個執行緒的 ThreadLocalMap 是屬於執行緒自己的,ThreadLocalMap 中維護的值也是屬於執行緒自己的。這就保證了 ThreadLocal 型別的變數在每個執行緒中是獨立的,在多執行緒環境下不會相互影響。

4. ThreadLocalMap

4.1 構造方法

ThreadLocal 中當前執行緒的 ThreadLocalMap 為 null 時會使用 ThreadLocalMap 的構造方法新建一個 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);
}複製程式碼

構造方法中會新建一個陣列,並將將第一次需要儲存的鍵值儲存到一個陣列中,完成一些初始化工作。

4.2 儲存結構

ThreadLocalMap 內部維護了一個雜湊表(陣列)來儲存資料,並且定義了載入因子:

// 初始容量,必須是 2 的冪
private static final int INITIAL_CAPACITY = 16;

// 儲存資料的雜湊表
private Entry[] table;

// table 中已儲存的條目數
private int size = 0;

// 表示一個閾值,當 table 中儲存的物件達到該值時就會擴容
private int threshold;

// 設定 threshold 的值
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}複製程式碼

table 是一個 Entry 型別的陣列,Entry 是 ThreadLocalMap 的一個內部類。

4.3 儲存物件 Entry

Entry 用於儲存一個鍵值對,其中 key 以弱引用的方式儲存:

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}複製程式碼

4.4 儲存鍵值對

呼叫 set(ThreadLocal key, Object value) 方法將資料儲存到雜湊表中:

private void set(ThreadLocal key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    // 計算要儲存的索引位置
    int i = key.threadLocalHashCode & (len-1);

    // 迴圈判斷要存放的索引位置是否已經存在 Entry,若存在,進入迴圈體
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal k = e.get();

        // 若索引位置的 Entry 的 key 和要儲存的 key 相等,則更新該 Entry 的值
        if (k == key) {
            e.value = value;
            return;
        }

        // 若索引位置的 Entry 的 key 為 null(key 已經被回收了),表示該位置的 Entry 已經無效,用要儲存的鍵值替換該位置上的 Entry
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 要存放的索引位置沒有 Entry,將當前鍵值作為一個 Entry 儲存在該位置
    tab[i] = new Entry(key, value);
    // 增加 table 儲存的條目數
    int sz = ++size;
    // 清除一些無效的條目並判斷 table 中的條目數是否已經超出閾值
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash(); // 調整 table 的容量,並重新擺放 table 中的 Entry
}複製程式碼

首先使用 key(當前 ThreadLocal)的 threadLocalHashCode 來計算要儲存的索引位置 i。threadLocalHashCode 的值由 ThreadLocal 類管理,每建立一個 ThreadLocal 物件都會自動生成一個相應的 threadLocalHashCode 值,其實現如下:

// ThreadLocal 物件的 HashCode
private final int threadLocalHashCode = nextHashCode();

// 使用 AtomicInteger 保證多執行緒環境下的同步
private static AtomicInteger nextHashCode =
    new AtomicInteger();

// 每次建立 ThreadLocal 物件是 HashCode 的增量
private static final int HASH_INCREMENT = 0x61c88647;

// 計算 ThreadLocal 物件的 HashCode
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}複製程式碼

在儲存資料時,如果索引位置有 Entry,且該 Entry 的 key 為 null,那麼就會執行清除無效 Entry 的操作,因為 Entry 的 key 使用的是弱引用的方式,key 如果被回收(即 key 為 null),這時就無法再訪問到 key 對應的 value,需要把這樣的無效 Entry 清除掉來騰出空間。

在調整 table 容量時,也會先清除無效物件,然後再根據需要擴容。

private void rehash() {
    // 先清除無效 Entry
    expungeStaleEntries();
    // 判斷當前 table 中的條目數是否超出了閾值的 3/4
    if (size >= threshold - threshold / 4)
        resize();
}複製程式碼

清除無用物件和擴容的方法這裡就不再展開說明了。

4.5 獲取 Entry 物件

取值是直接獲取到 Entry 物件,使用 getEntry(ThreadLocal key) 方法:

private Entry getEntry(ThreadLocal key) {
    // 使用指定的 key 的 HashCode 計算索引位置
    int i = key.threadLocalHashCode & (table.length - 1);
    // 獲取當前位置的 Entry
    Entry e = table[i];
    // 如果 Entry 不為 null 且 Entry 的 key 和 指定的 key 相等,則返回該 Entry
    // 否則呼叫 getEntryAfterMiss(ThreadLocal key, int i, Entry e) 方法
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}複製程式碼

因為可能存在雜湊衝突,key 對應的 Entry 的儲存位置可能不在通過 key 計算出的索引位置上,也就是說索引位置上的 Entry 不一定是 key 對應的 Entry。所以需要呼叫 getEntryAfterMiss(ThreadLocal key, int i, Entry e) 方法獲取。

private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    // 索引位置上的 Entry 不為 null 進入迴圈,為 null 則返回 null
    while (e != null) {
        ThreadLocal k = e.get();
        // 如果 Entry 的 key 和指定的 key 相等,則返回該 Entry
        if (k == key)
            return e;
        // 如果 Entry 的 key 為 null (key 已經被回收了),清除無效的 Entry
        // 否則獲取下一個位置的 Entry,迴圈判斷
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}複製程式碼

4.6 移除指定的 Entry

private void remove(ThreadLocal key) {
    Entry[] tab = table;
    int len = tab.length;
    // 使用指定的 key 的 HashCode 計算索引位置
    int i = key.threadLocalHashCode & (len-1);
    // 迴圈判斷索引位置的 Entry 是否為 null
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        // 若 Entry 的 key 和指定的 key 相等,執行刪除操作
        if (e.get() == key) {
            // 清除 Entry 的 key 的引用
            e.clear();
            // 清除無效的 Entry
            expungeStaleEntry(i);
            return;
        }
    }
}複製程式碼

4.7 記憶體洩漏

在 ThreadLocalMap 的 set(),get() 和 remove() 方法中,都有清除無效 Entry 的操作,這樣做是為了降低記憶體洩漏發生的可能。

Entry 中的 key 使用了弱引用的方式,這樣做是為了降低記憶體洩漏發生的概率,但不能完全避免記憶體洩漏。

這句話的意思好象是矛盾的,下面來分析一下。

假設 Entry 的 key 沒有使用弱引用的方式,而是使用了強引用:由於 ThreadLocalMap 的生命週期和當前執行緒一樣長,那麼當引用 ThreadLocal 的物件被回收後,由於 ThreadLocalMap 還持有 ThreadLocal 和對應 value 的強引用,ThreadLocal 和對應的 value 是不會被回收的,這就導致了記憶體洩漏。所以 Entry 以弱引用的方式避免了 ThreadLocal 沒有被回收而導致的記憶體洩漏,但是此時 value 仍然是無法回收的,依然會導致記憶體洩漏。

ThreadLocalMap 已經考慮到這種情況,並且有一些防護措施:在呼叫 ThreadLocal 的 get(),set() 和 remove() 的時候都會清除當前執行緒 ThreadLocalMap 中所有 key 為 null 的 value。這樣可以降低記憶體洩漏發生的概率。所以我們在使用 ThreadLocal 的時候,每次用完 ThreadLocal 都呼叫 remove() 方法,清除資料,防止記憶體洩漏。

相關文章