Java 之 ThreadLocal 詳解

1. 概念

ThreadLocal 用於提供線程局部變量,在多線程環境能夠保證各個線程裏的變量獨立於其它線程裏的變量。也就是說 ThreadLocal 能夠爲每一個線程建立一個【單獨的變量副本】,至關於線程的 private static 類型變量。java

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());
    }
}複製代碼

運行結果:bash

strLabel = child
threadLabel = main複製代碼

從運行結果能夠看出,對於 ThreadLocal 類型的變量,在一個線程中設置值,不影響其在其它線程中的值。也就是說 ThreadLocal 類型的變量的值在每一個線程中是獨立的。多線程

3. ThreadLocal 實現

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

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。this

ThreadLocalMap 至關於一個 HashMap,是真正保存值的地方。spa

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 的初始值。code

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() 方法,清除數據,防止內存泄漏。

相關文章
相關標籤/搜索