ThreadLocal原理記錄,別被坑了!!

半天想不出暱稱的斌發表於2020-12-10

簡介

ThreadLocal的用處

ThreadLocal是為了將資料記錄一份到某個執行緒裡,確保該資料執行緒安全

例如資料庫的Connection放入ThreadLocal,一個事務會用到很多DAO,但只能用共同的Connection,這樣才能保證事務完整性

所以當某個類的其中一個變數,會被同一個執行緒多次使用,並且還嚴格的規定每次都得是這個變數操作

那麼就能把這個變數放入ThreadLocal

Spring也是將各種Bean放入ThreadLocal中來確保Bean的“無狀態”化

吐槽開始!!

今天翻書看了關於ThreadLocal的介紹,和網上一些關於ThreadLocal的部落格,這個原理介紹真的是坑,大錯特錯

大家可能都看到過下面這種所謂的ThreadLocal簡單的實現思路介紹:

完了後還加上一句:

雖然上面的程式碼清單中的這個ThreadLocal實現版本顯得比較簡單粗爆,但其目的主要在與呈現JDK中所提供的ThreadLocal類在實現上的思路

上面這樣簡化ThreadLocal實現根本錯的離譜

不僅是有的部落格這樣,包括書本也是這樣介紹的,傳播知識給他人,的確可以簡化程式碼實現,但不等於更改了正確的實現思路!

這樣會誤導他人對ThreadLocal的進一步學習

ThreadLocal真正的實現方式

先說總結,跟上面錯誤做對比

1.ThreadLocalMap 別看有個Map結尾,其實壓根就是重新實現的類

   跟Map沒半毛錢關係,沒實現Map介面的,沒用HashMap,別覺得根據key找value就只能使用map了

2.執行緒根據key找對應的value,這個key並不是執行緒id,而是ThreadLocal類

   為什麼,因為ThreadLocalMap是存放線上程裡的,每個執行緒都只有一個只屬於自己的ThreadLocalMap

   這樣的話存個毛的執行緒id,有什麼意義?

揭開Thread,ThreadLocal,ThreadLocalMap真正的關係

首先進入ThreadLocal,發現如下

 ThreadLocalMap實在ThreadLocal裡實現的

 直接找到ThreadLocal的set()方法

    public void set(T value) {
        Thread t = Thread.currentThread();  //獲得當前執行緒
        ThreadLocalMap map = getMap(t);   //將當前執行緒作為引數傳入,來獲取ThreadLocalMap
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

然後進入getMap()方法

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;        //追蹤後發現t.threadLocals就是 ThreadLocal.ThreadLocalMap threadLocals;
    }

如果得到的map為null,那麼說明是第一次,走createMap方法建立

 void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);  //看到了吧,ThreadLocalMap直接是給Thread儲存的
    }

進入new ThreadLocalMap方法,這裡注意,傳入的this就是指ThreadLocal

 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {   //看到引數名字沒,firstKey,ThreadLocal傳進來是當作key值的!
            table = new Entry[INITIAL_CAPACITY];                 //table其實是private Entry[] table; 一個陣列而已
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

ThreadLocalMap沒有實現Map介面,跟Map沒半毛錢關係,至於Entry是什麼,我們看看

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

首先WeakReference是指弱引用的意思,繼承了這玩意有以下效果:

當一個物件僅僅被weak reference(弱引用)指向, 而沒有任何其他strong reference(強引用)指向的時候, 如果這時GC執行, 那麼這個物件就會被回收,不論當前的記憶體空間是否足夠,這個物件都會被回收。

好處在於,如果某個ThreadLocal被回收了,那麼ThreadLocalMap的這個Key在下次GC()的時候也會被回收(不然就造成記憶體洩露了,這個key永遠不會被呼叫)

 

Entry是一個ThreadLocalMap的內部類,跟Map的Entry實現有點像,有key和value的定義(俗稱 桶)

ok,我們發現,ThreadLocalMap建立後,就是初始化一個Entry的陣列,生成一個Entry將ThreadLocal作為key值,將要存的值作為value放入其中存好

那麼假設現在ThreadLocalMap已經存在,走的是第一個分支,直接set,我們看看他怎麼實現的

 1    private void set(ThreadLocal<?> key, Object value) {
 2 
 3             Entry[] tab = table;
 4             int len = tab.length;
 5             int i = key.threadLocalHashCode & (len-1);
 6 
 7             for (Entry e = tab[i];
 8                  e != null;
 9                  e = tab[i = nextIndex(i, len)]) {
10                 ThreadLocal<?> k = e.get();
11 
12                 if (k == key) {
13                     e.value = value;
14                     return;
15                 }
16 
17                 if (k == null) {
18                     replaceStaleEntry(key, value, i);
19                     return;
20                 }
21             }
22 
23             tab[i] = new Entry(key, value);
24             int sz = ++size;
25             if (!cleanSomeSlots(i, sz) && sz >= threshold)
26                 rehash();
27         }

看到第7行的for沒有,直接遍歷方才說的Entry陣列,將ThreadLocal取出來比較(相當於key比較),匹配就設定value,沒這個key就存起來

ThreadLocalMap為啥不用HashMap而是自己陣列實現

有key和value這個概念出現,也不是一定要用HashMap這些的,為什麼用陣列

個人覺得是省開銷,建立Map物件的開銷和使用Map的開銷,畢竟ThreadLocalMap初始預設長度為16,而真實情況中一個執行緒不會有這麼本地變數要儲存

 

所以,當使用ThreadLocal來存的時候,ThreadLocal會建立一個ThreadLocalMap給呼叫它的執行緒,自己作為key值去儲存

取值的時候,ThreadLocal拿Thread裡儲存的ThreadLocal,然後將自身作為key值去取值

為什麼ThreadLocalMap的程式碼不放在Thread中

網上有個答案我覺得很合理:

將ThreadLocalMap定義在Thread類內部看起來更符合邏輯
但是ThreadLocalMap並不需要Thread物件來操作,所以定義在Thread類內只會增加一些不必要的開銷。
定義在ThreadLocal類中的原因是ThreadLocal類負責ThreadLocalMap的建立和使用
總的來說就是,ThreadLocalMap不是必需品,定義在Thread中增加了成本,定義在ThreadLocal中按需建立。

執行緒不一定都用到ThreadLocal的哦,如果不使用,就不會被賦值一個ThreadLocalMap

換個思維,這其實也是種不錯的設計模式:

一個工具類把自身當作唯一標識,去操作工具類本身的方法,只需要將資料記錄給呼叫它的類就好

ThreadLocal的記憶體洩露問題

正常來說,我們建立一個執行緒,跑完後會銷燬,自動呼叫ThreadLocal的remove()方法,清除ThreadLocalMap的內容

但是,實際中我們是使用執行緒池的,而執行緒跑完後會返回執行緒池中,並不會銷燬

這時候的ThreadLocalMap的內容就還在的(記憶體就是這裡洩露啦)

所以,線上程池中用ThreadLocal,記得run()要跑完時用下remove()方法清除ThreadLocalMap中的key

 

至此分享完畢啦,希望大家點點贊,或者一鍵3連不迷路~~

相關文章