ThreadLocal源碼分析

ThreadLocal源碼分析

ThreadLocal的使用大部分場景是創建一個靜態的ThreadLocal對象,然後調用set、get方法,最後調用remove方法搞定。回過頭來想想,為何調用了set方法後,同一個線程的其他地方get的時候總能拿到set的值?沒有同步的變量為何在併發場景下屢次出錯?使用完ThreadLocal為何還要調用remove方法?連環異常OOM,究竟是哪次ThreadLocal調用所為?線上數百臺服務器意外雪崩背後又隱藏著什麼?是軟件的扭曲還是硬件的淪喪?走進科學帶你瞭解ThreadLocal的底層世界,各位看官請準備好瓜子板凳,且聽我一一道來。

ThreadLocal簡介

  • ThreadLocal並不是一個Thread,而是Thread的一個局部變量。
  • 變量值的共享可以使用public static的形式,所有線程都使用同一個變量,如果想實現每一個線程都有自己的共享變量該如何實現呢?JDK中的ThreadLocal類正是為了解決這樣的問題。
  • ThreadLocal類並不是用來解決多線程環境下的共享變量問題,而是用來提供線程內部的共享變量,在多線程環境下,可以保證各個線程之間的變量互相隔離、相互獨立。在線程中,可以通過get()/set()方法來訪問變量。ThreadLocal實例通常來說都是static類型的。這種變量在線程的生命週期內起作用,可以減少同一個線程內多個函數或者組件之間一些公共變量的傳遞的複雜度。
  • ThreadLocal類顧名思義可以理解為線程本地變量。也就是說如果定義了一個ThreadLocal,每個線程往這個ThreadLocal中讀寫是線程隔離,互相之間不會影響的。它提供了一種將可變數據通過每個線程有自己的獨立副本從而實現線程封閉的機制。

ThreadLocal用法

<code>public class ThreadLocalTest {    private static ThreadLocal<integer> threadLocal = new ThreadLocal<>();    public static void main(String[] args) {        // 啟動兩個線程測試        new DataThread(threadLocal).start();        new DataThread(threadLocal).start();    }    static class DataThread extends Thread {        private ThreadLocal<integer> threadLocal;        public DataThread(ThreadLocal<integer> threadLocal) {            this.threadLocal = threadLocal;        }        /**         * 設置值         *         * @param data         */        private void setData(int data) {            threadLocal.set(data);        }        /**         * 取值         *         * @return         */        private int getData() {            return threadLocal.get();        }        @Override        public void run() {            for (int i = 0; i < 5; i++) {                System.out.println(Thread.currentThread().getName() + "設置值:" + i);                setData(i);                System.out.println(Thread.currentThread().getName() + "獲取值:" + getData());            }            threadLocal.remove();        }    }}/<integer>/<integer>/<integer>/<code> 

執行結果:

<code>Thread-0設置值:0Thread-0獲取值:0Thread-0設置值:1Thread-0獲取值:1Thread-0設置值:2Thread-0獲取值:2Thread-0設置值:3Thread-0獲取值:3Thread-0設置值:4Thread-0獲取值:4Thread-1設置值:0Thread-1獲取值:0Thread-1設置值:1Thread-1獲取值:1Thread-1設置值:2Thread-1獲取值:2Thread-1設置值:3Thread-1獲取值:3Thread-1設置值:4Thread-1獲取值:4/<code>

這裡我們可以看到實例化了一個ThreadLocal對象,然後啟動了兩個線程分別執行了5次的set、get方法,每次get總能拿到set後的值,而且兩個線程雖然共享了ThreadLocal的實例,並沒有出現值錯誤的情況,如果線程執行完成,最好能再線程執行前顯示的調用了一次remove方法,避免OOM,後面會講到。

ThreadLocal源碼分析

看了以上的用法是不是很簡單?那麼ThreadLocal是怎麼實現變量跟線程綁定的呢,下面我們一起看看源碼是怎麼實現的。

ThreadLocal相關API

ThreadLocal源碼分析

我們可以看出它沒有對外暴露任何 變量,全部都是 private私有的,對外暴露的接口(public修飾)如下:

<code>// 無參構造方法,實例化一個ThreadLocalpublic ThreadLocal()// 獲取當前線程當前實例ThreadLocal中設置的值public T get()// 移除當前線程當前實例ThreadLocal中設置的值public void remove()// 設置當前線程的ThreadLocal實例中的值public void set(T value)// jdk1.8版本新特性,創建一個ThreadLocal實例public static  ThreadLocal withInitial(Supplier extends S> supplier)/<code>

SuppliedThreadLocal

這是一個內部類,主要是創建帶初始化的ThreadLocal時使用。

ThreadLocal源碼分析

<code>// 靜態final內部類,繼承了ThreadLocalstatic final class SuppliedThreadLocal extends ThreadLocal {    // jdk1.8新特性,是一個FunctionalInterface,只有一個get()返回對象    private final Supplier extends T> supplier;    SuppliedThreadLocal(Supplier extends T> supplier) {        this.supplier = Objects.requireNonNull(supplier);    }            // 覆蓋了ThreadLocal的initialValue方法設置初始值    @Override    protected T initialValue() {        return supplier.get();    }}/<code>

在ThreadLocal中使用方式如下:

<code>public static   withInitial(Supplier extends S> supplier) {    return new SuppliedThreadLocal<>(supplier);}// 用這個方法可以實例化ThreadLocal並設置初始值,這個內部類只會在ThreadLocal中使用,例如:ThreadLocal<integer> threadLocal = ThreadLocal.withInitial(() -> 2);// 這裡變量threadLocal實例化完成並設置了初始值2/<integer>/<code>

ThreadLocalMap

ThreadLocalMap是ThreadLocal的一個內部類,所有的操作都是這個類完成的,是ThreadLocal的核心類。

ThreadLocal源碼分析

在ThreadLocalMap類中還有一個靜態類Entry,鍵值對的數據結構,key為ThreadLocal實例,value為要存放的值,後面會講到。

下面介紹一下ThreadLocalMap的核心API接口。

<code>// 設置擴容閾值private void setThreshold(int len)// 計算下一個存儲的數據下標位置,其實是個環形,使用線性探測法private static int nextIndex(int i, int len)// 計算上一個存儲的數據下標位置,其實是個環形,使用線性探測法private static int prevIndex(int i, int len)// 構造函數:當前ThreadLocal實例和要存放的值為參數ThreadLocalMap(ThreadLocal> firstKey, Object firstValue)// 從父線程繼承過來的ThreadLocalMap構造函數,僅僅是在初始化使用,後期父子線程的ThreadLocalMap各自維護自己的ThreadLocalMapprivate ThreadLocalMap(ThreadLocalMap parentMap)// 獲取存放指定ThreadLocal的Entryprivate Entry getEntry(ThreadLocal> key)// 未找到指定ThreadLocal的Entry時,採用線性探測法繼續查找Entryprivate Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e)// 設置指定ThreadLocal的值private void set(ThreadLocal> key, Object value)// 刪除指定ThreadLocal的Entryprivate void remove(ThreadLocal> key)// 替換無效的ThreadLocal的值,並清除無效的Entryprivate void replaceStaleEntry(ThreadLocal> key, Object value,int staleSlot)// 清除無效的Entryprivate int expungeStaleEntry(int staleSlot)// 清除區間內無效的Entryprivate boolean cleanSomeSlots(int i, int n)// set方法調用後如果Entry數據裡的存放值size大於閾值了要進行一次清除無效的Entry,如果已使用的數組大小仍然大於3/4閾值時需要做擴容private void rehash()// 擴容,大小為數組的2倍private void resize()// 清除全部無效的Entry,再做rehash會調用private void expungeStaleEntries()/<code>

源碼分析

threadLocalHashCode變量

這是ThreadLocal類內部的一個final修飾的變量,查找ThreadLocal在Entry數組中下標時起到關鍵作用。

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

我們可以看到 threadLocalHashCode的值使用原子類 AtomicInteger自增外加一個固定值 HASH_INCREMENT得到的,這個固定值為什麼是 0x61c88647呢,這是一個魔數,在構建ThreadLocalMap的時候會計算存儲的下標: inti=firstKey.threadLocalHashCode&(INITIAL_CAPACITY-1);,當我們用 0x61c88647作為魔數累加為每個ThreadLocal分配各自的ID也就是threadLocalHashCode再與2的冪取模,得到的結果分佈很均勻。ThreadLocalMap使用的是線性探測法,均勻分佈的好處在於很快就能探測到下一個臨近的可用slot,從而保證效率。我們可以做如下測試

<code>private static void test5() {    getHashIndex(32);    System.out.println("===========");    getHashIndex(64);}public static void getHashIndex(int len) {    AtomicInteger atomicInteger = new AtomicInteger();    int hash_increment = 0x61c88647;    List<integer> list = new ArrayList<>();    for (int i = 0; i < len; i++) {        list.add(atomicInteger.getAndAdd(hash_increment) & (len - 1));    }    System.out.println("數組大小為" + len + "時計算的下標:" + list);    Collections.sort(list);    System.out.println("數組大小為" + len + "時計算的下標排序後:" + list);}/<integer>/<code>

得到的結果是:

<code>數組大小為32時計算的下標:[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]數組大小為32時計算的下標排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]===========數組大小為64時計算的下標:[0, 7, 14, 21, 28, 35, 42, 49, 56, 63, 6, 13, 20, 27, 34, 41, 48, 55, 62, 5, 12, 19, 26, 33, 40, 47, 54, 61, 4, 11, 18, 25, 32, 39, 46, 53, 60, 3, 10, 17, 24, 31, 38, 45, 52, 59, 2, 9, 16, 23, 30, 37, 44, 51, 58, 1, 8, 15, 22, 29, 36, 43, 50, 57]數組大小為64時計算的下標排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]/<code>

由此可以看出正是由於 0x61c88647這個魔數的作用使得hash後的值能均勻的分佈在2的冪次方大小數組裡,大大降低了hash衝突的問題,這個魔數的選取與斐波那契散列有關,有興趣的可以參考:https://www.jianshu.com/p/36edfa202182 或者 https://www.cct.lsu.edu/~sidhanti/tutorials/data_structures/page214.html

ThreadLocalMap.Entry

<code>static class Entry extends WeakReference<threadlocal>> {    /** The value associated with this ThreadLocal. */    Object value;    Entry(ThreadLocal> k, Object v) {      super(k);      value = v;    }}/<threadlocal>/<code>

這裡Entry繼承了 WeakReference, WeakReference在Java裡是一種弱引用,在JVM內存不夠的情況下,發生了GC垃圾回收,這部分的變量是會被回收掉的。如果這裡使用普通的key-value形式來定義存儲結構,實質上就會造成節點的生命週期與線程強綁定,只要線程沒有銷燬,那麼節點在GC分析中一直處於可達狀態,沒辦法被回收,而程序本身也無法判斷是否可以清理節點。弱引用是Java中四檔引用的第三檔,比軟引用更加弱一些,如果一個對象沒有強引用鏈可達,那麼一般活不過下一次GC。當某個ThreadLocal已經沒有強引用可達,則隨著它被垃圾回收,在ThreadLocalMap裡對應的Entry的鍵值會失效,這為ThreadLocalMap本身的垃圾清理提供了便利。關於Java強引用、軟引用、弱引用、虛引用大家可以自行搜索答案。

InheritableThreadLocal

<code>public class InheritableThreadLocal extends ThreadLocalstatic ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap)/<code>

InheritableThreadLocal繼承了ThreadLocal,InheritableThreadLocal提供了一種父子線程之間的數據共享機制,這裡主要是在Thread類裡執行init初始化線程的時候調用。

<code>if (inheritThreadLocals && parent.inheritableThreadLocals != null)    this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);/<code>

ThreadLocalMap

這個ThreadLocalMap是核心類,是一個自定義的類似Map的接口,但是跟HashMap還是有很大區別的,不要理解差了,所有的底層操作就是在這個類裡完成的,這裡我們重點介紹。我們先看看ThreadLocal類裡幾個方法。

<code>public T get() {    // 獲取到當前線程    Thread t = Thread.currentThread();    // 獲取當前線程的ThreadLocalMap    ThreadLocalMap map = getMap(t);    if (map != null) {        // 找到當前ThreadLocal所在的Entry        ThreadLocalMap.Entry e = map.getEntry(this);        if (e != null) {            @SuppressWarnings("unchecked")            // 當前ThreadLocal存在,則直接返回value值            T result = (T)e.value;            return result;        }    }    // 這裡需要注意的是,如果沒有調用過set方法,則第一次調用get方法時會返回默認的初始化的值    return setInitialValue();}public void set(T value) {    // 獲取到當前線程    Thread t = Thread.currentThread();    // 獲取當前線程的ThreadLocalMap    ThreadLocalMap map = getMap(t);    // 當前線程的ThreadLocalMap存在,直接設置新值    if (map != null)        map.set(this, value);    else        // 不存在時需要實例化一個ThreadLocalMap,並把value值放進Entry        createMap(t, value);}public void remove() {    ThreadLocalMap m = getMap(Thread.currentThread());    if (m != null)        m.remove(this);}ThreadLocalMap getMap(Thread t) {    // 直接返回線程的內部變量threadLocals    return t.threadLocals;}/<code>

我們看到這裡的get、set、remove方法都是先獲取到ThreadLocalMap然後再進行相應的操作,這裡 getMap方法使用的參數就是當前線程對象 Thread.currentThread(),獲取的就是Thread類裡的threadLocals變量,這個 變量就是 ThreadLocal.ThreadLocalMap。


ThreadLocal源碼分析

從這裡就說明了ThreadLocal.ThreadLocalMap是跟當前線程綁定的,所以存取都是在同一個線程裡完成的,相當於線程的本次變量副本,線程隔離,不受其他線程影響。

<code>/*** The initial capacity -- MUST be a power of two.* 初始的Entry數組大小是16,必須是2的冪次方,就是之前解釋的使用魔數在2的冪次方里做hash能均勻分佈*/private static final int INITIAL_CAPACITY = 16;/*** The table, resized as necessary.* table.length MUST always be a power of two.* 這個數組裡存儲的就是鍵值對,key為ThreadLocal實例,value就是設置的變量值*/private Entry[] table;/*** The number of entries in the table.* Entry數組裡已存放的數據大小*/private int size = 0;/*** The next size value at which to resize.* 擴容的閾值,一般達到數組長度的2/3就開始擴容*/private int threshold; // Default to 0/*** Set the resize threshold to maintain at worst a 2/3 load factor.* 一般達到數據長度的2/3就擴容*/private void setThreshold(int len) {    threshold = len * 2 / 3;}/<code> 

在ThreadLocal首次放入值的時候會創建ThreadLocalMap實例,ThreadLocal會調用ThreadLocalMap的createMap方法,如下所示。

<code>void createMap(Thread t, T firstValue) {    t.threadLocals = new ThreadLocalMap(this, firstValue);}/<code>

調用的ThreadLocal的構造方法實現是這樣的

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

這裡會首先初始化一個Entry數組,然後計算當前ThreadLocal所落在的下標並設置,然後設置擴容閾值, firstKey.threadLocalHashCode&(INITIAL_CAPACITY-1),對於2的冪作為模數取模,可以用&(2^n-1)來替代%2^n,位運算比取模效率高很多,因為對2^n取模,只要不是低n位對結果的貢獻顯然都是0,會影響結果的只能是低n位。

下面我們重點看ThreadLocalMap的get、set、remove方法。

<code>private void set(ThreadLocal> key, Object value) {    Entry[] tab = table;    int len = tab.length;    // 計算參數ThreadLocal實例所存放的數組下標    int i = key.threadLocalHashCode & (len-1);    // 這裡使用了循環,如果計算的下標已經有數據了並且不是當前對象,也就是hash衝突了,那麼就使用nextIndex線性探測法繼續往下找存放位置    for (Entry e = tab[i];         e != null;         e = tab[i = nextIndex(i, len)]) {        ThreadLocal> k = e.get();        // 如果參數ThreadLocal和計算下標得到的是同一個對象,直接設置value並返回        if (k == key) {            e.value = value;            return;        }        // 如果get出來的ThreadLocal為null,前面說了Entry繼承了WeakReference,弱引用的ThreadLocal可能被垃圾回收了,所以需要替換掉無效的Entry        if (k == null) {            replaceStaleEntry(key, value, i);            return;        }    }    // 如果以上都不符合時,直接創建一個Entry對象並放入下標i中,累加size,如果沒有清除任何無效的Entry並且數據大小超過了閾值threshold,那麼需要進行擴容    tab[i] = new Entry(key, value);    int sz = ++size;    if (!cleanSomeSlots(i, sz) && sz >= threshold)        rehash();}private Entry getEntry(ThreadLocal> key) {        // 計算下標    int i = key.threadLocalHashCode & (table.length - 1);    Entry e = table[i];    // 如果下標存放的就是當前ThreadLocal時,直接返回這個Entry,否則使用線性探測法繼續查找    if (e != null && e.get() == key)        return e;    else        return getEntryAfterMiss(key, i, e);}// 這個方法就是第一次計算下標沒有找到ThreadLocal時,使用線性探測法繼續向後查找,如果遇到Entry的key為null的情況,做一次清理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;}// 刪除ThreadLocal中的值private void remove(ThreadLocal> key) {    Entry[] tab = table;    int len = tab.length;    int i = key.threadLocalHashCode & (len-1);    // 先計算出hash值確定下標,如果下標的Threadl對象正是查找的對象則將Entry裡的value置為null,然後以當前下標為起點執行一次清除無效Entry,垃圾回收時可回收掉,如果當前下標不是該對象則線性探測法繼續向後查找。操作類似。    for (Entry e = tab[i];         e != null;         e = tab[i = nextIndex(i, len)]) {        if (e.get() == key) {            e.clear();            expungeStaleEntry(i);            return;        }    }}// Entry數組裡的size大於擴容閾值時會重新rehash,清除無效Entry後,如果size還大於閾值的3/4時就進行擴容private void rehash() {    expungeStaleEntries();    // Use lower threshold for doubling to avoid hysteresis    if (size >= threshold - threshold / 4)        resize();}// 擴容2倍,一般都是2的冪次方,Entry擴大2倍後,裡面的數據會重新計算存儲下標,衝突採用線性探測法解決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;}/<code> 

還有幾個跟垃圾回收後ThreadLocalMap相關處理的方法,主要是set、get方法遇到ThreadLocal為null的情況的一些處理,包括區間清除無效Entry、全量清除無效Entry。

<code>// set方法調用的時候,如果ThreadLocal為null,會觸發這個方法,放入新值private void replaceStaleEntry(ThreadLocal> key, Object value,int staleSlot)// 清除無效的Entryprivate int expungeStaleEntry(int staleSlot)// 清除某一區間無效的Entryprivate boolean cleanSomeSlots(int i, int n)/<code>

ThreadLocal應用場景

  • ThreadLocal在spring、hibernate、mybatis中用來解決數據庫連接、事務管理、Session管理等,
  • ThreadLocal對象通常用於防止對可變的單實例變量或全局變量進行共享。例如:日期格式化 SimpleDateFormat或者JDBC的 Connection等都不是線程安全的,因此,當多個線程應用程序在沒有協同的情況下,使用全局變量時,就是線程不安全的。通過將對象保存到ThreadLocal中,每個線程都會擁有自己的對象副本,線程間隔離操作互不影響。
  • 其他一些需要程序員在代碼中保證線程私有變量副本的實例對象的應該創建ThreadLocal。

ThreadLocal內存洩漏

  • 認為ThreadLocal會引起內存洩漏的說法是因為如果一個ThreadLocal對象被回收了,我們往裡面放的value對於【當前線程->當前線程的threadLocals(ThreadLocal.ThreadLocalMap對象)->Entry數組->某個entry.value】這樣一條強引用鏈是可達的,因此value不會被回收。
  • 認為ThreadLocal不會引起內存洩漏的說法是因為ThreadLocal.ThreadLocalMap源碼實現中自帶一套自我清理的機制。
  • 這就導致了一個問題,ThreadLocal在沒有外部強引用時,發生GC時會被回收,如果創建ThreadLocal的線程一直持續運行,那麼這個Entry對象中的value就有可能一直得不到回收,發生內存洩露。
  • ThreadLocal在線程池中使用時比如自己創建的線程池或者web服務器(Tomcat等)採用線程池機制,也就是說線程是可以複用的,所以在每次進入的時候都需要重新進行set操作,或者使用完畢以後及時remove掉!
  • 如何避免內存洩露呢,既然已經發現有內存洩露的隱患,自然有應對的策略,在調用ThreadLocal的get()、set()可能會清除ThreadLocalMap中key為null的Entry對象,這樣對應的value就沒有GC Roots可達了,下次GC的時候就可以被回收,當然如果調用remove方法,肯定會刪除對應的Entry對象。如果使用ThreadLocal的set方法之後,沒有顯示的調用remove方法,就有可能發生內存洩露,所以養成良好的編程習慣十分重要,使用完ThreadLocal之後,記得顯示調用remove方法。

總結

  • ThredLocal為每個線程保存一個自己的變量,但其實ThreadLocal本身並不存儲變量,變量存儲在線程自己的實例變量ThreadLocal.ThreadLocalMap threadLocals中
  • ThreadLocal的設計並不是為了解決併發問題,而是解決一個變量在線程內部的共享問題,在線程內部處處可以訪問
  • 因為每個線程都只會訪問自己ThreadLocalMap 保存的變量,所以不存在線程安全問題
  • ThreadLocal源碼相對較少,很適合作為入門源碼分析,建議多看幾遍,很多其他框架裡都大量的使用了ThreadLocal,掌握了ThreadLocal後理解其他的框架應用設計思想就容易多了。


分享到:


相關文章: