阿里架構師淺析ThreadLocal源碼——黃金分割數的使用

一. 前提

最近接觸到的一個項目要兼容新老系統,最終採用了ThreadLocal(實際上用的是InheritableThreadLocal)用於在子線程獲取父線程中共享的變量。問題是解決了,可是後來發現對ThreadLocal的理解不夠深刻,因而順便把它的源碼閱讀理解了一遍。在談到ThreadLocal以前先買個關子,先談談黃金分割數。本文在閱讀ThreadLocal源碼的時候是使用JDK8(1.8.0_181)。java

二. 黃金分割數與斐波那契數列

首先複習一下斐波那契數列,下面的推導過程來自某搜索引擎的wiki:算法

  • 斐波那契數列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …
  • 通項公式:假設F(n)爲該數列的第n項(n ∈ N*),那麼這句話能夠寫成以下形式:F(n) = F(n-1) + F(n-2)。

有趣的是,這樣一個徹底是天然數的數列,通項公式倒是用無理數來表達的。並且當n趨向於無窮大時,前一項與後一項的比值愈來愈逼近0.618(或者說後一項與前一項的比值小數部分愈來愈逼近0.618),而這個值0.618就被稱爲黃金分割數。證實過程以下:編程

黃金分割數的準確值爲(根號5 - 1)/2,約等於0.618。併發

三. 黃金分割數的應用

黃金分割數被普遍使用在美術、攝影等藝術領域,由於它具備嚴格的比例性、藝術性、和諧性,蘊藏着豐富的美學價值,可以激發人的美感。固然,這些不是本文研究的方向,咱們先嚐試求出無符號整型和帶符號整型的黃金分割數的具體值:框架

public static void main(String[] args) throws Exception {
    //黃金分割數 * 2的32次方 = 2654435769 - 這個是無符號32位整數的黃金分割數對應的那個值
	long c = (long) ((1L << 32) * (Math.sqrt(5) - 1) / 2);
	System.out.println(c);
    //強制轉換爲帶符號爲的32位整型,值爲-1640531527
	int i = (int) c;
	System.out.println(i);
}

經過一個線段圖理解一下:ide

也就是2654435769爲32位無符號整數的黃金分割值,而-1640531527就是32位帶符號整數的黃金分割值。而ThreadLocal中的哈希魔數正是1640531527(十六進制爲0x61c88647)。爲何要使用0x61c88647做爲哈希魔數?這裏提早說一下ThreadLocal在ThreadLocalMap(ThreadLocal在ThreadLocalMap以Key的形式存在)中的哈希求Key下標的規則:函數

哈希算法:keyIndex = ((i + 1) * HASH_INCREMENT) & (length - 1)源碼分析

其中,i爲ThreadLocal實例的個數,這裏的HASH_INCREMENT就是哈希魔數0x61c88647,length爲ThreadLocalMap中可容納的Entry(K-V結構)的個數(或者稱爲容量)。在ThreadLocal中的內部類ThreadLocalMap的初始化容量爲16,擴容後老是2的冪次方,所以咱們能夠寫個Demo模擬整個哈希的過程:性能

public class Main {
	
	private static final int HASH_INCREMENT = 0x61c88647;

	public static void main(String[] args) throws Exception {
		hashCode(4);
		hashCode(16);
		hashCode(32);
	}

	private static void hashCode(int capacity) throws Exception {
		int keyIndex;
		for (int i = 0; i < capacity; i++) {
			keyIndex = ((i + 1) * HASH_INCREMENT) & (capacity - 1);
			System.out.print(keyIndex);
			System.out.print(" ");
		}
		System.out.println();
	}
}

上面的例子中,咱們分別模擬了ThreadLocalMap容量爲4,16,32的狀況下,不觸發擴容,而且分別」放入」4,16,32個元素到容器中,輸出結果以下:學習

3 2 1 0 
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0

每組的元素通過散列算法後剛好填充滿了整個容器,也就是實現了完美散列。實際上,這個並非偶然,其實整個哈希算法能夠轉換爲多項式證實:證實(x - y) * HASH_INCREMENT != 2^n * (n m),在x != y,n != m,HASH_INCREMENT爲奇數的狀況下恆成立,具體證實能夠自行完成。HASH_INCREMENT賦值爲0x61c88647的API文檔註釋以下:

連續生成的哈希碼之間的差別(增量值),將隱式順序線程本地id轉換爲幾乎最佳分佈的乘法哈希值,這些不一樣的哈希值最終生成一個2的冪次方的哈希表。

四. ThreadLocal是什麼

下面引用ThreadLocal的API註釋:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID)

稍微翻譯一下:ThreadLocal提供線程局部變量。這些變量與正常的變量不一樣,由於每個線程在訪問ThreadLocal實例的時候(經過其get或set方法)都有本身的、獨立初始化的變量副本。ThreadLocal實例一般是類中的私有靜態字段,使用它的目的是但願將狀態(例如,用戶ID或事務ID)與線程關聯起來。

ThreadLocal由Java界的兩個大師級的做者編寫,Josh Bloch和Doug Lea。Josh Bloch是JDK5語言加強、Java集合(Collection)框架的創辦人以及《Effective Java》系列的做者。Doug Lea是JUC(java.util.concurrent)包的做者,Java併發編程的泰斗。因此,ThreadLocal的源碼十分值得學習。

五. ThreadLocal的原理

ThreadLocal雖然叫線程本地(局部)變量,可是實際上它並不存聽任何的信息,能夠這樣理解:它是線程(Thread)操做ThreadLocalMap中存放的變量的橋樑。它主要提供了初始化、set()、get()、remove()幾個方法。這樣說可能有點抽象,下面畫個圖說明一下在線程中使用ThreadLocal實例的set()和get()方法的簡單流程圖。

假設咱們有以下的代碼,主線程的線程名字是main(也有可能不是main):

public class Main {
	
	private static final ThreadLocal<String> LOCAL = new ThreadLocal<>();
	
	public static void main(String[] args) throws Exception{
		LOCAL.set("doge");
		System.out.println(LOCAL.get());
	}
}

上面只描述了單線程的狀況而且由於是主線程忽略了Thread t = new Thread()這一步,若是有多個線程會稍微複雜一些,可是原理是不變的,ThreadLocal實例老是經過Thread.currentThread()獲取到當前操做線程實例,而後去操做線程實例中的ThreadLocalMap類型的成員變量,所以它是一個橋樑,自己不具有存儲功能。

六. ThreadLocal源碼分析

對於ThreadLocal的源碼,咱們須要重點關注set()、get()、remove()幾個方法。

1. ThreadLocal的內部屬性

//獲取下一個ThreadLocal實例的哈希魔數
private final int threadLocalHashCode = nextHashCode();

//原子計數器,主要到它被定義爲靜態
private static AtomicInteger nextHashCode = new AtomicInteger();

//哈希魔數(增加數),也是帶符號的32位整型值黃金分割值的取正
private static final int HASH_INCREMENT = 0x61c88647;

//生成下一個哈希魔數
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

這裏須要注意一點,threadLocalHashCode是一個final的屬性,而原子計數器變量nextHashCode和生成下一個哈希魔數的方法nextHashCode()是靜態變量和靜態方法,靜態變量只會初始化一次。換而言之,每新建一個ThreadLocal實例,它內部的threadLocalHashCode就會增長0x61c88647。舉個例子:

//t1中的threadLocalHashCode變量爲0x61c88647
ThreadLocal t1 = new ThreadLocal();
//t2中的threadLocalHashCode變量爲0x61c88647 + 0x61c88647
ThreadLocal t2 = new ThreadLocal();
//t3中的threadLocalHashCode變量爲0x61c88647 + 0x61c88647 + 0x61c88647
ThreadLocal t3 = new ThreadLocal();

threadLocalHashCode是下面的ThreadLocalMap結構中使用的哈希算法的核心變量,對於每一個ThreadLocal實例,它的threadLocalHashCode是惟一的。

2. 內部類ThreadLocalMap的基本結構和源碼分析

ThreadLocal內部類ThreadLocalMap使用了默認修飾符,也就是包(包私有)可訪問的。ThreadLocalMap內部定義了一個靜態類Entry。咱們重點看下ThreadLocalMap的源碼,先當作員和結構部分:

/**
 * ThreadLocalMap是一個定製的散列映射,僅適用於維護線程本地變量。
 * 它的全部方法都是定義在ThreadLocal類以內。
 * 它是包私有的,因此在Thread類中能夠定義ThreadLocalMap做爲變量。
 * 爲了處理很是大(指的是值)和長時間的用途,哈希表的Key使用了弱引用(WeakReferences)。
 * 引用的隊列(弱引用)再也不被使用的時候,對應的過時的條目就能經過主動刪除移出哈希表。
 */
static class ThreadLocalMap {
 
        //注意這裏的Entry的Key爲WeakReference<ThreadLocal<?>>
	static class Entry extends WeakReference<ThreadLocal<?>> {
        
		//這個是真正的存放的值
		Object value;
                // Entry的Key就是ThreadLocal實例自己,Value就是輸入的值
		Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
	}
        //初始化容量,必須是2的冪次方
	private static final int INITIAL_CAPACITY = 16;

        //哈希(Entry)表,必須時擴容,長度必須爲2的冪次方
	private Entry[] table;

        //哈希表中元素(Entry)的個數
	private int size = 0;
 
        //下一次須要擴容的閾值,默認值爲0
	private int threshold;
   
        //設置下一次須要擴容的閾值,設置值爲輸入值len的三分之二
	private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }
    
    // 以len爲模增長i
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }
     
    // 以len爲模減小i
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }
}

這裏注意到十分重要的一點:ThreadLocalMap$Entry是WeakReference(弱引用),而且鍵值Key爲ThreadLocal<?>實例自己,這裏使用了無限定的泛型通配符。

接着看ThreadLocalMap的構造函數:

// 構造ThreadLocal時候使用,對應ThreadLocal的實例方法void createMap(Thread t, T firstValue)
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 哈希表默認容量爲16
    table = new Entry[INITIAL_CAPACITY];
    // 計算第一個元素的哈希碼
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

// 構造InheritableThreadLocal時候使用,基於父線程的ThreadLocalMap裏面的內容進行提取放入新的ThreadLocalMap的哈希表中
// 對應ThreadLocal的靜態方法static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap)
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
    // 基於父ThreadLocalMap的哈希表進行拷貝
    for (Entry e : parentTable) {
        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++;
            }
        }
    }
}

這裏注意一下,ThreadLocal的set()方法調用的時候會懶初始化一個ThreadLocalMap而且放入第一個元素。而ThreadLocalMap的私有構造是提供給靜態方法ThreadLocal#createInheritedMap()使用的。

接着看ThreadLocalMap提供給ThreadLocal使用的一些實例方法:

// 若是Key在哈希表中找不到哈希槽的時候會調用此方法
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 這裏會經過nextIndex嘗試遍歷整個哈希表,若是找到匹配的Key則返回Entry
    // 若是哈希表中存在Key == null的狀況,調用expungeStaleEntry進行清理
    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;
}

// 1.清空staleSlot對應哈希槽的Key和Value
// 2.對staleSlot到下一個空的哈希槽之間的全部可能衝突的哈希表部分槽進行重哈希,置空Key爲null的槽
// 3.注意返回值是staleSlot以後的下一個空的哈希槽的哈希碼
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    
    // expunge entry at staleSlot
    // 清空staleSlot對應哈希槽的Key和Value
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    // 下面的過程是對staleSlot到下一個空的哈希槽之間的全部可能衝突的哈希表部分槽進行重哈希,置空Key爲null的槽
    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 {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

// 這裏個方法比較長,做用是替換哈希碼爲staleSlot的哈希槽中Entry的值
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).
    int slotToExpunge = staleSlot;
    // 這個循環主要是爲了找到staleSlot以前的最前面的一個Key爲null的哈希槽的哈希碼
    for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // Find either the key or trailing null slot of run, whichever
    // occurs first
    // 遍歷staleSlot以後的哈希槽,若是Key匹配則用輸入值替換
    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        if (k == key) {
            e.value = value;
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
    // Key匹配不了,則新建立一個哈希槽
    // If key not found, put new entry in stale slot
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
    
    // 這裏若是當前的staleSlot和找到前置的slotToExpunge不一致會進行一次清理
    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

// 對當前哈希表中全部的Key爲null的Entry調用expungeStaleEntry
private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

// 清理第i個哈希槽以後的n個哈希槽,若是遍歷的時候發現Entry的Key爲null,則n會重置爲哈希表的長度,expungeStaleEntry有可能會重哈希使得哈希表長度發生變化
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;
}


/**
 * 這個方法主要給`ThreadLocal#get()`調用,經過當前ThreadLocal實例獲取哈希表中對應的Entry
 *
 */
private Entry getEntry(ThreadLocal<?> key) {
    // 計算Entry的哈希值
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i]; 
    if (e != null && e.get() == key)
        return e;
    else  // 注意這裏,若是e爲null或者Key對不上,會調用getEntryAfterMiss
        return getEntryAfterMiss(key, i, e);
}

// 重哈希,必要時進行擴容
private void rehash() {
    // 清理全部空的哈希槽,而且進行重哈希
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    // 哈希表的哈希元素個數大於3/4閾值時候觸發擴容
    if (size >= threshold - threshold / 4)
        resize();
}

// 擴容,簡單的擴大2倍的容量        
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (Entry e : oldTab) {
        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;
}

// 基於ThreadLocal做爲key,對當前的哈希表設置值,此方法由`ThreadLocal#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();
        // Key匹配,直接設置值
        if (k == key) {
            e.value = value;
            return;
        }
        // 若是Entry的Key爲null,則替換該Key爲當前的key,而且設置值
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 清理當前新設置元素的哈希槽下標到sz段的哈希槽,若是清理成功而且sz大於閾值則觸發擴容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

簡單來講,ThreadLocalMap是ThreadLocal真正的數據存儲容器,實際上ThreadLocal數據操做的複雜部分的全部邏輯都在ThreadLocalMap中進行,而ThreadLocalMap實例是Thread的成員變量,在ThreadLocal#set()方法首次調用的時候設置到當前執行的線程實例中。若是在同一個線程中使用多個ThreadLocal實例,實際上,每一個ThreadLocal實例對應的是ThreadLocalMap的哈希表中的一個哈希槽。舉個例子,在主函數主線程中使用多個ThreadLocal實例:

public class ThreadLocalMain {

	private static final ThreadLocal<Integer> TL_1 = new ThreadLocal<>();
	private static final ThreadLocal<String> TL_2 = new ThreadLocal<>();
	private static final ThreadLocal<Long> TL_3 = new ThreadLocal<>();

	public static void main(String[] args) throws Exception {
		TL_1.set(1);
		TL_2.set("1");
		TL_3.set(1L);
		Field field = Thread.class.getDeclaredField("threadLocals");
		field.setAccessible(true);
		Object o = field.get(Thread.currentThread());
		System.out.println(o);
	}
}

實際上,主線程的threadLocals屬性中的哈希表中通常不止咱們上面定義的三個ThreadLocal,由於加載主線程的時候還有可能在其餘地方使用到ThreadLocal,筆者某次Debug的結果以下:

用PPT畫圖簡化一下:

上圖threadLocalHashCode屬性一行的表是爲了標出每一個Entry的哈希槽的哈希值,實際上,threadLocalHashCode是ThreadLocal@XXXX中的一個屬性,這是很顯然的,原本threadLocalHashCode就是ThreadLocal的一個成員變量。

上面只是簡單粗略對ThreadLocalMap的源碼進行了流水帳的分析,下文會做一些詳細的圖,說明一下ThreadLocal和ThreadLocalMap中的一些核心操做的過程。

3. ThreadLocal的建立

從ThreadLocal的構造函數來看,ThreadLocal實例的構造並不會作任何操做,只是爲了獲得一個ThreadLocal的泛型實例,後續能夠把它做爲ThreadLocalMap$Entry的鍵:

// 注意threadLocalHashCode在每一個新`ThreadLocal`實例的構造同時已經肯定了
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

// 經過Supplier去覆蓋initialValue方法
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

// 默認公有構造函數
public ThreadLocal() {

}

注意threadLocalHashCode在每一個新ThreadLocal實例的構造同時已經肯定了,這個值也是Entry哈希表的哈希槽綁定的哈希值。

4. TreadLocal的set方法

ThreadLocal中set()方法的源碼以下:

public void set(T value) {
	//設置值前老是獲取當前線程實例
    Thread t = Thread.currentThread();
	//從當前線程實例中獲取threadLocals屬性
    ThreadLocalMap map = getMap(t);
    if (map != null)
	     //threadLocals屬性不爲null則覆蓋key爲當前的ThreadLocal實例,值爲value
         map.set(this, value);
    else
	//threadLocals屬性爲null,則建立ThreadLocalMap,第一個項的Key爲當前的ThreadLocal實例,值爲value
        createMap(t, value);
}

// 這裏看到獲取ThreadLocalMap實例時候老是從線程實例的成員變量獲取
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 建立ThreadLocalMap實例的時候,會把新實例賦值到線程實例的threadLocals成員
void createMap(Thread t, T firstValue) {
     t.threadLocals = new ThreadLocalMap(this, firstValue);
}

上面的過程源碼很簡單,設置值的時候老是先獲取當前線程實例而且操做它的變量threadLocals。步驟是:

  • 獲取當前運行線程的實例。
  • 經過線程實例獲取線程實例成員threadLocals(ThreadLocalMap),若是爲null,則建立一個新的ThreadLocalMap實例賦值到threadLocals。
  • 經過threadLocals設置值value,若是原來的哈希槽已經存在值,則進行覆蓋。

5. TreadLocal的get方法

ThreadLocal中get()方法的源碼以下:

public T get() {
   //獲取當前線程的實例
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    //根據當前的ThreadLocal實例獲取ThreadLocalMap中的Entry,使用的是ThreadLocalMap的getEntry方法
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T) e.value;
             return result;
            }
        }
    //線程實例中的threadLocals爲null,則調用initialValue方法,而且建立ThreadLocalMap賦值到threadLocals
    return setInitialValue();
}

private T setInitialValue() {
    // 調用initialValue方法獲取值
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // ThreadLocalMap若是未初始化則進行一次建立,已初始化則直接設置值
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

protected T initialValue() {
     return null;
}

initialValue()方法默認返回null,若是ThreadLocal實例沒有使用過set()方法直接使用get()方法,那麼ThreadLocalMap中的此ThreadLocal爲Key的項會把值設置爲initialValue()方法的返回值。若是想改變這個邏輯能夠對initialValue()方法進行覆蓋。

6. TreadLocal的remove方法

ThreadLocal中remove()方法的源碼以下:

public void remove() {
	//獲取Thread實例中的ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
	   //根據當前ThreadLocal做爲Key對ThreadLocalMap的元素進行移除
       m.remove(this);
}

七. ThreadLocal.ThreadLocalMap的初始化

咱們能夠關注一下java.lang.Thread類裏面的變量:

public class Thread implements Runnable {

  //傳遞ThreadLocal中的ThreadLocalMap變量
  ThreadLocal.ThreadLocalMap threadLocals = null;
  //傳遞InheritableThreadLocal中的ThreadLocalMap變量
  ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

也就是,ThreadLocal須要存放和獲取的數據實際上綁定在Thread實例的成員變量threadLocals中,而且是ThreadLocal#set()方法調用的時候才進行懶加載的,能夠結合上一節的內容理解一下,這裏不展開。

八. 什麼狀況下ThreadLocal的使用會致使內存泄漏

其實ThreadLocal自己不存聽任何的數據,而ThreadLocal中的數據其實是存放在線程實例中,從實際來看是線程內存泄漏,底層來看是Thread對象中的成員變量threadLocals持有大量的K-V結構,而且線程一直處於活躍狀態致使變量threadLocals沒法釋放被回收。threadLocals持有大量的K-V結構這一點的前提是要存在大量的ThreadLocal實例的定義,通常來講,一個應用不可能定義大量的ThreadLocal,因此通常的泄漏源是線程一直處於活躍狀態致使變量threadLocals沒法釋放被回收。可是咱們知道,·ThreadLocalMap·中的Entry結構的Key用到了弱引用(·WeakReference<ThreadLocal<?>>·),當沒有強引用來引用ThreadLocal實例的時候,JVM的GC會回收ThreadLocalMap中的這些Key,此時,ThreadLocalMap中會出現一些Key爲null,可是Value不爲null的Entry項,這些Entry項若是不主動清理,就會一直駐留在ThreadLocalMap中。也就是爲何ThreadLocal中get()、set()、remove()這些方法中都存在清理ThreadLocalMap實例key爲null的代碼塊。總結下來,內存泄漏可能出現的地方是:

  • 大量地(靜態)初始化ThreadLocal實例,初始化以後再也不調用get()、set()、remove()方法。

  • 初始化了大量的ThreadLocal,這些ThreadLocal中存放了容量大的Value,而且使用了這些ThreadLocal實例的線程一直處於活躍的狀態。

ThreadLocal中一個設計亮點是ThreadLocalMap中的Entry結構的Key用到了弱引用。試想若是使用強引用,等於ThreadLocalMap中的全部數據都是與Thread的生命週期綁定,這樣很容易出現由於大量線程持續活躍致使的內存泄漏。使用了弱引用的話,JVM觸發GC回收弱引用後,ThreadLocal在下一次調用get()、set()、remove()方法就能夠刪除那些ThreadLocalMap中Key爲null的值,起到了惰性刪除釋放內存的做用。

其實ThreadLocal在設置內部類ThreadLocal.ThreadLocalMap中構建的Entry哈希表已經考慮到內存泄漏的問題,因此ThreadLocal.ThreadLocalMap$Entry類設計爲弱引用,類簽名爲static class Entry extends WeakReference<ThreadLocal<?>>。以前一篇文章介紹過,若是弱引用關聯的對象若是置爲null,那麼該弱引用會在下一次GC時候回收弱引用關聯的對象。舉個例子:

public class ThreadLocalMain {

	private static ThreadLocal<Integer> TL_1 = new ThreadLocal<>();

	public static void main(String[] args) throws Exception {
		TL_1.set(1);
		TL_1 = null;
		System.gc();
		Thread.sleep(300);
	}
}

這種狀況下,TL_1這個ThreadLocal在主動GC以後,線程綁定的ThreadLocal.ThreadLocalMap實例中的Entry哈希表中原來的TL_1所在的哈希槽Entry的引用持有值referent(繼承自WeakReference)會變成null,可是Entry中的value是強引用,還存放着TL_1這個ThreadLocal未回收以前的值。這些被」孤立」的哈希槽Entry就是前面說到的要惰性刪除的哈希槽。

九. ThreadLocal的最佳實踐

其實ThreadLocal的最佳實踐很簡單:

  • 每次使用完ThreadLocal實例,都調用它的remove()方法,清除Entry中的數據。

調用remove()方法最佳時機是線程運行結束以前的finally代碼塊中調用,這樣能徹底避免操做不當致使的內存泄漏,這種主動清理的方式比惰性刪除有效。

十. 小結

ThreadLocal線程本地變量是線程實例傳遞和存儲共享變量的橋樑,真正的共享變量仍是存放在線程實例自己的屬性中。ThreadLocal裏面的基本邏輯並不複雜,可是一旦涉及到性能影響、內存回收(弱引用)和惰性刪除等環節,其實它考慮到的東西仍是相對全面並且有效的。

寫在最後

  • 第一:看完點贊,感謝您的承認;
  • ...
  • 第二:隨手轉發,分享知識,讓更多人學習到;
  • ...
  • 第三:記得點關注,天天更新的!!!
  • ...

相關文章
相關標籤/搜索