(八)ThreadLocal的使用及原理分析

什麼是ThreadLocal

ThreadLocal,簡單翻譯過來就是本地線程,可是直接這麼翻譯很難理解ThreadLocal的做用,若是換一種說法,能夠稱爲線程本地存儲。簡單來講,就是ThreadLocal爲共享變量在每一個線程中都建立一個副本,每一個線程能夠訪問本身內部的副本變量。這樣作的好處是能夠保證共享變量在多線程環境下訪問的線程安全性java

ThreadLocal的使用

沒有使用ThreadLocal時

經過一個簡單的例子來演示一下ThreadLocal的做用,這段代碼是定義了一個靜態的成員變量num,而後經過構造5個線程對這個num作遞增算法

public class ThreadLocalDemo {

    private static Integer num=0;

    public static void main(String[] args) {
        Thread[] threads=new Thread[5];
        for(int i=0;i<5;i++){
            threads[i]=new Thread(()->{
               num+=5;
               System.out.println(Thread.currentThread().getName()+" : "+num);
            },"Thread-"+i);
        }

        for(Thread thread:threads){
            thread.start();
        }
    }
}

運行結果數據庫

Thread-0 : 5
Thread-1 : 10
Thread-2 : 15
Thread-3 : 20
Thread-4 : 25

每一個線程都會對這個成員變量作遞增,若是線程的執行順序不肯定,那麼意味着每一個線程得到的結果也是不同的。數組

使用了ThreadLocal之後

經過ThreadLocal對上面的代碼作一個改動安全

public class ThreadLocalDemo {

    private static final ThreadLocal<Integer> local=new ThreadLocal<Integer>(){
        protected Integer initialValue(){
            return 0; //經過initialValue方法設置默認值
        }
    };

    public static void main(String[] args) {
        Thread[] threads=new Thread[5];
        for(int i=0;i<5;i++){
            threads[i]=new Thread(()->{
                int num=local.get().intValue();
                num+=5;
               System.out.println(Thread.currentThread().getName()+" : "+num);
            },"Thread-"+i);
        }

        for(Thread thread:threads){
            thread.start();
        }
    }
}

運行結果微信

Thread-0 : 5
Thread-4 : 5
Thread-2 : 5
Thread-1 : 5
Thread-3 : 5

從結果能夠看到,每一個線程的值都是5,意味着各個線程之間都是獨立的變量副本,彼此不相互影響.session

ThreadLocal會給定一個初始值,也就是 initialValue()方法,而每一個線程都會從ThreadLocal中得到這個初始化的值的副本,這樣可使得每一個線程都擁有一個副本拷貝

看到這裏,估計有不少人都會和我同樣有一些疑問多線程

  1. 每一個線程的變量副本是怎麼存儲的?
  2. ThreadLocal是如何實現多線程場景下的共享變量副本隔離?

帶着疑問,來看一下ThreadLocal這個類的定義(默認狀況下,JDK的源碼都是基於1.8版本)函數

ThreadLocal的全局圖

從ThreadLocal的方法定義來看,仍是挺簡單的。就幾個方法源碼分析

  • get: 獲取ThreadLocal中當前線程對應的線程局部變量
  • set:設置當前線程的線程局部變量的值
  • remove:將當前線程局部變量的值刪除

另外,還有一個initialValue()方法,在前面的代碼中有演示,做用是返回當前線程局部變量的初始值,這個方法是一個protected方法,主要是在構造ThreadLocal時用於設置默認的初始值

set方法的實現

set方法是設置一個線程的局部變量的值,至關於當前線程經過set設置的局部變量的值,只對當前線程可見。

public void set(T value) {
        Thread t = Thread.currentThread();//獲取當前執行的線程
        ThreadLocalMap map = getMap(t); //得到當前線程的ThreadLocalMap實例
        if (map != null)//若是map不爲空,說明當前線程已經有了一個ThreadLocalMap實例
            map.set(this, value);//直接將當前value設置到ThreadLocalMap中
        else
            createMap(t, value); //說明當前線程是第一次使用線程本地變量,構造map
    }
  • Thread.currentThread 獲取當前執行的線程
  • getMap(t) ,根據當前線程獲得當前線程的ThreadLocalMap對象,這個對象具體是作什麼的?稍後分析
  • 若是map不爲空,說明當前線程已經構造過ThreadLocalMap,直接將值存儲到map中
  • 若是map爲空,說明是第一次使用,調用createMap構造

ThreadLocalMap是什麼?

咱們來分析一下這句話,ThreadLocalMap map=getMap(t)得到一個ThreadLocalMap對象,那這個對象是幹嗎的呢?
其實不用分析,基本上也能猜想出來,Map是一個集合,集合用來存儲數據,那麼在ThreadLocal中,應該就是用來存儲線程的局部變量的。ThreadLocalMap這個類很關鍵。

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

t.threadLocals實際上就是訪問Thread類中的ThreadLocalMap這個成員變量

public
class Thread implements Runnable {
 /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
...
}

從上面的代碼發現每個線程都有本身單獨的ThreadLocalMap實例,而對應這個線程的全部本地變量都會保存到這個map內

ThreadLocalMap是在哪裏構造?

set方法中,有一行代碼createmap(t,value);,這個方法就是用來構造ThreadLocalMap,從傳入的參數來看,它的實現邏輯基本也能猜出出幾分吧

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

Thread t 是經過Thread.currentThread()來獲取的表示當前線程,而後直接經過new ThreadLocalMap將當前線程中的threadLocals作了初始化
ThreadLocalMap是一個靜態內部類,內部定義了一個Entry對象用來真正存儲數據

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;
            }
        }
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            //構造一個Entry數組,並設置初始大小
            table = new Entry[INITIAL_CAPACITY];
            //計算Entry數據下標
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //將`firstValue`存入到指定的table下標中
            table[i] = new Entry(firstKey, firstValue);
            size = 1;//設置節點長度爲1
            setThreshold(INITIAL_CAPACITY); //設置擴容的閾值
        }
    //...省略部分代碼
}
分析到這裏,基本知道了ThreadLocalMap長啥樣了,也知道它是如何構造的?那麼我看到這裏的時候仍然有疑問
  • Entry集成了WeakReference,這個表示什麼意思?
  • 在構造ThreadLocalMap的時候new ThreadLocalMap(this, firstValue);,key實際上是this,this表示當前對象的引用,在當前的案例中,this指的是ThreadLocal<Integer> local。那麼多個線程對應同一個ThreadLocal實例,怎麼對每個ThreadLocal對象作區分呢?

解惑WeakReference

weakReference表示弱引用,在Java中有四種引用類型,強引用、弱引用、軟引用、虛引用。
使用弱引用的對象,不會阻止它所指向的對象被垃圾回收器回收。

在Java語言中, 當一個對象o被建立時, 它被放在Heap裏. 當GC運行的時候, 若是發現沒有任何引用指向o, o就會被回收以騰出內存空間. 也就是說, 一個對象被回收, 必須知足兩個條件:

  • 沒有任何引用指向它
  • GC被運行.

這段代碼中,構造了兩個對象a,b,a是對象DemoA的引用,b是對象DemoB的引用,對象DemoB同時還依賴對象DemoA,那麼這個時候咱們認爲從對象DemoB是能夠到達對象DemoA的。這種稱爲強可達(strongly reachable)

DemoA a=new DemoA();
DemoB b=new DemoB(a);

若是咱們增長一行代碼來將a對象的引用設置爲null,當一個對象再也不被其餘對象引用的時候,是會被GC回收的,可是對於這個場景來講,即時是a=null,也不可能被回收,由於DemoB依賴DemoA,這個時候是可能形成內存泄漏的

DemoA a=new DemoA();
DemoB b=new DemoB(a);
a=null;

經過弱引用,有兩個方法能夠避免這樣的問題

//方法1
DemoA a=new DemoA();
DemoB b=new DemoB(a);
a=null;
b=null;
//方法2
DemoA a=new DemoA();
WeakReference b=new WeakReference(a);
a=null;

對於方法2來講,DemoA只是被弱引用依賴,假設垃圾收集器在某個時間點決定一個對象是弱可達的(weakly reachable)(也就是說當前指向它的全都是弱引用),這時垃圾收集器會清除全部指向該對象的弱引用,而後把這個弱可達對象標記爲可終結(finalizable)的,這樣它隨後就會被回收。

試想一下若是這裏沒有使用弱引用,意味着ThreadLocal的生命週期和線程是強綁定,只要線程沒有銷燬,那麼ThreadLocal一直沒法回收。而使用弱引用之後,當ThreadLocal被回收時,因爲Entry的key是弱引用,不會影響ThreadLocal的回收防止內存泄漏,同時,在後續的源碼分析中會看到,ThreadLocalMap自己的垃圾清理會用到這一個好處,方便對無效的Entry進行回收

解惑ThreadLocalMap以this做爲key

在構造ThreadLocalMap時,使用this做爲key來存儲,那麼對於同一個ThreadLocal對象,若是同一個Thread中存儲了多個值,是如何來區分存儲的呢?
答案就在firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)

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

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

關鍵點就在threadLocalHashCode,它至關於一個ThreadLocal的ID,實現的邏輯以下

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

這裏用到了一個很是完美的散列算法,能夠簡單理解爲,對於同一個ThreadLocal下的多個線程來講,當任意線程調用set方法存入一個數據到Entry中的時候,其實會根據threadLocalHashCode生成一個惟一的id標識對應這個數據,存儲在Entry數據下標中。

  • threadLocalHashCode是經過nextHashCode.getAndAdd(HASH_INCREMENT)來實現的

i*HASH_INCREMENT+HASH_INCREMENT,每次新增一個元素(ThreadLocal)到Entry[],都會自增0x61c88647,目的爲了讓哈希碼能均勻的分佈在2的N次方的數組裏

  • Entry[i]= hashCode & (length-1)

魔數0x61c88647

從上面的分析能夠看出,它是在上一個被構造出的ThreadLocal的threadLocalHashCode的基礎上加上一個魔數0x61c88647。咱們來作一個實驗,看看這個散列算法的運算結果

private static final int HASH_INCREMENT = 0x61c88647;
    public static void main(String[] args) {
        magicHash(16); //初始大小16
        magicHash(32); //擴容一倍
    }

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

輸出結果

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

根據運行結果,這個算法在長度爲2的N次方的數組上,確實能夠完美散列,沒有任何衝突, 是否是很神奇。

魔數0x61c88647的選取和斐波那契散列有關, 0x61c88647對應的十進制爲1640531527。而斐波那契散列的乘數能夠用 (long) ((1L << 31) * (Math.sqrt(5) - 1)); 若是把這個值給轉爲帶符號的int,則會獲得-1640531527。也就是說
(long) ((1L << 31) * (Math.sqrt(5) - 1));獲得的結果就是1640531527,也就是魔數 0x61c88647
//(根號5-1)*2的31次方=(根號5-1)/2 *2的32次方=黃金分割數*2的32次方
long l1 = (long) ((1L << 31) * (Math.sqrt(5) - 1));
System.out.println("32位無符號整數: " + l1);
int i1 = (int) l1;
System.out.println("32位有符號整數:   " + i1);
總結,咱們用0x61c88647做爲魔數累加爲每一個ThreadLocal分配各自的ID也就是threadLocalHashCode再與2的冪取模,獲得的結果分佈很均勻。

圖形分析

爲了更直觀的體現set方法的實現,經過一個圖形表示以下

set方法創造的ThreadLocalMap結構

set剩餘源碼分析

前面分析了set方法第一次初始化ThreadLocalMap的過程,也對ThreadLocalMap的結構有了一個全面的瞭解。那麼接下來看一下map不爲空時的執行邏輯

private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            // 根據哈希碼和數組長度求元素放置的位置,即數組下標
            int i = key.threadLocalHashCode & (len-1);
             //從i開始日後一直遍歷到數組最後一個Entry(線性探索)
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                 //若是key相等,覆蓋value
                if (k == key) {
                    e.value = value;
                    return;
                }
                 //若是key爲null,用新key、value覆蓋,同時清理歷史key=null的陳舊數據
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
             //若是超過閥值,就須要擴容了
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

主要邏輯

  • 根據key的散列哈希計算Entry的數組下標
  • 經過線性探索探測從i開始日後一直遍歷到數組的最後一個Entry
  • 若是map中的key和傳入的key相等,表示該數據已經存在,直接覆蓋
  • 若是map中的key爲空,則用新的key、value覆蓋,並清理key=null的數據
  • rehash擴容

replaceStaleEntry

因爲Entry的key爲弱引用,若是key爲空,說明ThreadLocal這個對象被GC回收了。
replaceStaleEntry的做用就是把陳舊的Entry進行替換

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

           //向前掃描,查找最前一個無效的slot
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                   //經過循環遍歷,能夠定位到最前面一個無效的slot
                    slotToExpunge = i; 

            //從i開始日後一直遍歷到數組最後一個Entry(線性探索)
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                //找到匹配的key之後
                if (k == key) {
                    e.value = value;//更新對應slot的value值
                    //與無效的sloat進行交換
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    //若是最先的一個無效的slot和當前的staleSlot相等,則從i做爲清理的起點
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    //從slotToExpunge開始作一次連續的清理
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

               
                //若是當前的slot已經無效,而且向前掃描過程當中沒有無效slot,則更新slotToExpunge爲當前位置
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            //若是key對應的value在entry中不存在,則直接放一個新的entry
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

           //若是有任何一個無效的slot,則作一次清理
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

cleanSomeSlots

這個函數有兩處地方會被調用,用於清理無效的Entry

  • 插入的時候可能會被調用
  • 替換無效slot的時候可能會被調用

區別是前者傳入的n爲元素個數,後者爲table的容量

private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                 // i在任何狀況下本身都不會是一個無效slot,因此從下一個開始判斷
                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;
        }

expungeStaleEntry

執行一次全量清理

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

            // expunge entry at staleSlot
            tab[staleSlot].value = null;//刪除value
            tab[staleSlot] = null;//刪除entry
            size--; //map的size遞減

            // Rehash until we encounter 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) {//key爲null,執行刪除操做
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {//key不爲null,從新計算下標
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {//若是不在同一個位置
                        tab[i] = null;//把老位置的entry置null(刪除)

                        // 從h開始日後遍歷,一直到找到空爲止,插入
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

get操做

set的邏輯分析完成之後,get的源碼分析就很簡單了

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")
                T result = (T)e.value;
                return result;
            }
        }
        //若是map爲null,即尚未初始化,走初始化方法
        return setInitialValue();
    }

setInitialValue

根據initialValue()的value初始化ThreadLocalMap

private T setInitialValue() {
        T value = initialValue();//protected方法,用戶能夠重寫
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //若是map不爲null,把初始化value設置進去
            map.set(this, value);
        else
            //若是map爲null,則new一個map,並把初始化value設置進去
            createMap(t, value);
        return value;
    }
  • 從當前線程中獲取ThreadLocalMap,查詢當前ThreadLocal變量實例對應的Entry,若是不爲null,獲取value,返回
  • 若是map爲null,即尚未初始化,走初始化方法

remove方法

remove的方法比較簡單,從Entry[]中刪除指定的key就行

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

     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();//調用Entry的clear方法
                    expungeStaleEntry(i);//清除陳舊數據
                    return;
                }
            }
        }

應用場景

ThreadLocal的實際應用場景:

  1. 好比在線程級別,維護session,維護用戶登陸信息userID(登錄時插入,多個地方獲取)
  2. 數據庫的連接對象Connection,能夠經過ThreadLocal來作隔離避免線程安全問題

問題

ThreadLocal的內存泄漏

ThreadLocalMap中Entry的key使用的是ThreadLocal的弱引用,若是一個ThreadLocal沒有外部強引用,當系統執行GC時,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現一個key爲null的Entry,而這個key=null的Entry是沒法訪問的,當這個線程一直沒有結束的話,那麼就會存在一條強引用鏈

圖片描述

Thread Ref - > Thread -> ThreadLocalMap - > Entry -> value 永遠沒法回收而形成內存泄漏

其實咱們從源碼分析能夠看到,ThreadLocalMap是作了防禦措施的
  • 首先從ThreadLocal的直接索引位置(經過ThreadLocal.threadLocalHashCode & (len-1)運算獲得)獲取Entry e,若是e不爲null而且key相同則返回e
  • 若是e爲null或者key不一致則向下一個位置查詢,若是下一個位置的key和當前須要查詢的key相等,則返回對應的Entry,不然,若是key值爲null,則擦除該位置的Entry,不然繼續向下一個位置查詢

在這個過程當中遇到的key爲null的Entry都會被擦除,那麼Entry內的value也就沒有強引用鏈,天然會被回收。仔細研究代碼能夠發現,set操做也有相似的思想,將key爲null的這些Entry都刪除,防止內存泄露。
可是這個設計一來與一個前提條件,就是調用get或者set方法,可是不是全部場景都會知足這個場景的,因此爲了不這類的問題,咱們能夠在合適的位置手動調用ThreadLocal的remove函數刪除不須要的ThreadLocal,防止出現內存泄漏

因此建議的使用方法是
  • 將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命週期就更長,因爲一直存在ThreadLocal的強引用,因此ThreadLocal也就不會被回收,也就能保證任什麼時候候都能根據ThreadLocal的弱引用訪問到Entry的value值,而後remove它,防止內存泄露
  • 每次使用完ThreadLocal,都調用它的remove()方法,清除數據。

掃碼關注微信

相關文章
相關標籤/搜索