ThreadLocal 原始碼解讀

2021-08-20 09:43:47 字數 4150 閱讀 1393

在正式讀**前先簡單介紹threadlocal的實現方式。每個執行緒都會有乙個threadlocalmap,只有在使用到threadlocal的時候才會初始化threadlocalmap。需要儲存的物件t會被放到entry裡面儲存在threadlocalmap的陣列中,entry是乙個鍵值對的資料結構,threadlocal例項為key,t為value。在使用的過程中,threadlocal會先找到當前執行緒的threadlocalmap,根據threadlocal的雜湊值找到儲存的位置執行get方法或者set方法。

下面我畫了一張圖來說明。threadlocal本身不是用來存放資料的,真正用來儲存的是執行緒內部的threadlocalmap,而threadlocal只是作為threadlocalmap中的key。

老樣子,還是由一段簡單的**開始深入原始碼

final threadlocalthreadlocal = new threadlocal<>();

threadlocal.set("你好");

log.d("mark", "mark_1:" + threadlocal.get());

new thread(new runnable()

}).start();

05-31 16:58:30.878 19235-19235/com.newhongbin.lalala d/mark: mark_1:你好

05-31 16:58:30.879 19235-19626/com.newhongbin.lalala d/mark: mark_2:null

05-31 16:58:30.879 19235-19626/com.newhongbin.lalala d/mark: mark_3:很高興見到你

首先在主線程中建立threadlocal物件,並set「你好」,在主線程中get,可以看到取到的就是剛才set的字串;然後開啟乙個子執行緒,這時候子執行緒中還沒有set過,所以取出來的是null,在子執行緒set過之後,就能夠成功取出相應的字串了。雖然是同乙個threadlocal物件,但是在不同的執行緒中get到的資料是不一樣的。

順著set方法一**竟:

public void set(t value)
邏輯很簡單,取到當前執行緒的threadlocalmap,如果沒有初始化過,就呼叫createmap初始化。初始化過程就是呼叫threadlocalmap的其中乙個構造方法,我們來看看這個構造方法。

threadlocalmap(threadlocal firstkey, object firstvalue)
構造方法中會定義乙個entry陣列,陣列初始化容量為16,擴容因子為2/3,每次擴容為原來的2倍。entry是weakreference的子類,因此不會影響threadlocal物件的生命週期以及記憶體**。entry實現了鍵值對儲存的功能,當前threadlocal物件為key,需要儲存的物件為value。

static class entry extends weakreference

}

初始化完entry陣列之後,需要計算當前threadlocal的雜湊值(hashcode),因為這裡是第乙個放入的entry,不可能會發生hash碰撞,所以計算完hashcode之後,就直接把entry放入陣列下標為hashcode的位置上。最後計算出下一次需要擴容的臨界值,即 (陣列長度*2/3) 。到此為止,第乙個值成功set。

那麼問題又來了:

1、如果乙個threadlocal在同乙個執行緒中多次set呢?

2、如果多個threadlocal在同乙個執行緒中的hashcode一樣怎麼辦呢?

ok,回到剛開始的set方法,如果threadlocalmap不為null的情況。

private void set(threadlocal key, object value) 

if (k == null)

}//在空的位置上放入entry之前先判斷是否需要擴容

tab[i] = new entry(key, value);

int sz = ++size;

if (!cleansomeslots(i, sz) && sz >= threshold)

rehash();

}

set方法中關鍵的幾個步驟我都在原始碼中加了注釋,應該比較容易理解,這樣就能回答上面的兩個問題:

1、如果乙個threadlocal在同乙個執行緒中多次set呢?

直接覆蓋原有的值。

2、如果多個threadlocal在同乙個執行緒中的hashcode一樣怎麼辦呢?

如果發生碰撞的那個位置上的entry的threadlocal被**了,就放在碰撞的位置上;如果沒有被**,就尋找entry下乙個位置進行判斷,直到找到threadlocal被**的entry,或者空的位置,放入。

前面的set其實已經把threadlocal的基本實現方式全部梳理清楚了,所以接下來的get方法看起來會更容易些。

public t get() 

return setinitialvalue();

}

如果當前執行緒在get之前已經初始化過threadlocalmap,那麼就根據hashcode找到指定的entry,返回value。

如果當前執行緒在get之前還未初始化過threadlocalmap,那麼就返回setinitialvalue()。

private t setinitialvalue() 

protected t initialvalue()

setinitialvalue方法很簡單,定義乙個value指向null,如果threadlocalmap不為空,就插入value;如果threadlocalmap為空,先呼叫createmap初始化threadloaclmap,再插入value。最後返回的就是value。

前面分析完get和set差不多就理解threadlocal的實現原理了,當然實際的**還是比這更複雜一些的,threadlocalmap針對entry陣列還有清理方法,擦除方法,替換方法等,不過核心差不多,都是遍歷檢視entry的key有效性,做出相應的處理,我就不再把**展開了。但是可以看到這些方法裡面都會清除threadlocal已經無效的那個value,這裡面涉及到一些記憶體的問題,這裡也來分析一下。

前面說了threadlocal作為key是以弱引用的方式儲存在entry裡面的,一旦發生gc,key就被**了,那麼value就無法被訪問了,但是呢,value還有一條引用鏈,即「thread->threadlocalmap->entry->value」,所以value無法被**,卻也無法被訪問,導致記憶體的洩露。為了儘量減少這種情況,在get、set等方法裡面,都會去處理這一類key被**的entry。

藉著threadlocalmap也想聊聊擴容這個方法,一般的像hashmap、threadlocalmap等以鍵值對儲存的容器類都有乙個擴容方法,而且相似的是,容器的初始大小都是2^n,擴容也是2倍,這樣設定的作用是啥呢?

所有的物件都有hashcode,而且一般來說不同的物件hashcode也不同,值的分布會相對比較均勻。那麼來看看threadlocalmap計算儲存下標的演算法:

int i = key.threadlocalhashcode & (table.length - 1);
hashcode & (2^n-1)

假設n為4,即i = hashcode & 1111,只要hashcode在陣列容量以內不相同,計算的陣列下標i就不會相同。

換乙個情況,如果容量不為2^n,假設為17,i = hashcode & 10000,這種情況下當hashcode大於0小於16,得到的陣列下標i都是0,這樣的分布是不是想想就覺得可怕。

threadlocal的hashcode也很有意思,第一次類載入的時候會初始化乙個靜態的atomicinteger,後續每建立乙個threadlocal都會改變這個atomicinteger,這樣就能夠減少threadlocal的hashcode碰撞的概率。

如有不對,敬請指教。溜了溜了……

ThreadLocal原始碼理解

threadlocal其實原理是建立了多份相同資料儲存在堆記憶體上,每個執行緒的thread類裡有threadlocal.threadlocalmap threadlocals的屬性來指向存位置,所以每個執行緒修改都不會影響到其他執行緒的資料 首先說下下面用到的東西 threadlocalmap為t...

ThreadLocal原始碼分析

在理解handler looper之前,先來說說threadlocal這個類,聽名字好像是乙個本地執行緒的意思,實際上它並不是乙個thread,而是提供乙個與執行緒有關的區域性變數功能,每個執行緒之間的資料互不影響。我們知道使用handler的時候,每個執行緒都需要有乙個looper物件,那麼and...

ThreadLocal原始碼分析

threadlocal使用的常見場景 1 登入使用者資訊的存放 usercontext持有乙個threadlocal 2 框架中 事務需要將connection放入threadlocal 保證多個 dao或者service操作 被外層的service的時候使用同乙個connection達到事務效果 ...