天天看點

ThreadLocal應用及源碼分析

ThreadLocal

ThreadLocal 的作用是:提供線程内的局部變量,不同的線程之間不會互相幹擾,這種變量線上程的生命周期内起作用,減少同一個線程内多個函數或元件之間一些公共變量傳遞的複雜度,降低耦合性。

方法聲明

描述

ThreadLocal()

建立ThreadLocal對象

public void set( T value)

設定目前線程綁定的局部變量

public T get()

擷取目前線程綁定的局部變量

public void remove()

移除目前線程綁定的局部變量

簡單使用:

ThreadLocal應用及源碼分析
ThreadLocal應用及源碼分析

這樣可以很好的解決多線程之間資料隔離的問題,用synchronized加鎖也可以實作,但synchronized側重的是多個線程之間通路資源的同步性,而ThreadLocal側重的是每個線程之間的資料隔離。

synchronized

原理

同步機制采用'以時間換空間'的方式, 隻提供了一份變量,讓不同的線程排隊通路

ThreadLocal采用'以空間換時間'的方式, 為每一個線程都提供了一份變量的副本,進而實作同時通路而相不幹擾

側重點

多個線程之間通路資源的同步性

多線程中讓每個線程之間的資料互相隔離

涉及到資料傳遞和線程隔離的場景,可以考慮用ThreadLocal來解決:轉賬案例,涉及兩個DML操作: 一個轉出,一個轉入。這些操作是需要具備原子性的。是以這裡就需要操作事務,來保證轉出和轉入操作具備原子性。開啟事務的注意兩點:

為了保證所有的操作在一個事務中, 使用的連接配接必須是同一個: service層開啟事務的connection需要跟dao層通路資料庫的connection保持一緻。

線程并發情況下, 每個線程隻能操作各自的 connection。

用ThreadLocal的解決方案:在擷取Connection連接配接的JdbcUtils工具類加入ThreadLocal,代碼如下:

可以看出使用ThreadLocal的好處:

傳遞資料 : 儲存每個線程綁定的資料,在需要的地方可以直接擷取, 避免參數直接傳遞帶來的代碼耦合問題

線程隔離 : 各線程之間的資料互相隔離卻又具備并發性,避免同步方式帶來的性能損失

jdk8以前:

ThreadLocal應用及源碼分析

jdk8之前使用ThreadLocal來維護一個ThreadLocalMap,以線程作為key

jdk8以後:

ThreadLocal應用及源碼分析

jdk8之後使用Thread來維護一個ThreadLocalMap,以ThreadLocal作為key

這樣涉及的好處:

(1) 每個<code>Map</code>存儲的<code>Entry</code>數量就會變少,因為jdk8之前的存儲數量由<code>Thread</code>的數量決定,現在是由<code>ThreadLocal</code>的數量決定。

(2) 當<code>Thread</code>銷毀之後,對應的<code>ThreadLocalMap</code>也會随之銷毀,能減少記憶體的使用。

protected T initialValue()

傳回目前線程局部變量的初始值

首先調用<code>Thread.currentThread()</code>方法擷取目前線程對象,然後根據目前線程擷取維護的<code>ThreadLocalMap</code>對象;如果擷取的<code>Map</code>不為空,則在Map中以<code>ThreadLocal</code>的引用作為key,調用<code>getEntry</code>擷取對應的存儲實體,如果Entry不為空,擷取對應的 value值。如果Map為空或者Entry為空,則調用<code>setInitialValue()</code>方法。setInitialValue()方法裡,調用<code>initialValue()</code>方法擷取初始化值value,然後判斷目前線程是否有<code>ThreadLocalMap</code>,map存在,調用<code>set</code>設定Entry;map不存在則調用<code>createMap()</code>進行ThreadLocalMap對象的初始化,并将此<code>entry</code>作為第一個值存放至ThreadLocalMap中。

​ A. 首先擷取目前線程,并根據目前線程擷取一個ThreadLocalMap

​ B. 如果擷取的Map不為空,則将參數設定到Map中(目前ThreadLocal的引用作為key)

​ C. 如果Map為空,則調用createMap給該線程建立 Map,并設定初始值

A. 首先擷取目前線程,并根據目前線程擷取一個ThreadLocalMap

B. 如果擷取的Map不為空,則移除目前ThreadLocal對象對應的entry

(1) 這個方法是一個延遲調用方法,在set方法還未調用而先調用了get方法時才執行,并且僅執行1次。

(2)這個方法直接傳回一個<code>null</code>。

(3)如果想要一個除null之外的初始值,可以重寫此方法。(備注: 該方法是一個<code>protected</code>的方法,顯然是為了讓子類覆寫而設計的)

ThreadLocalMap是ThreadLocal的内部類,沒有實作Map接口,用獨立的方式實作了Map的功能,其内部的Entry也是獨立實作。

ThreadLocal應用及源碼分析

成員變量

Entry

在ThreadLocalMap中,也是用Entry來儲存K-V結構資料的。但是Entry中key隻能是ThreadLocal對象,這點被Entry的構造方法已經限定死了;

另外,Entry繼承WeakReference,使用弱引用,可以将ThreadLocal對象的生命周期和線程生命周期解綁,持有對ThreadLocal的弱引用,可以使得ThreadLocal在沒有其他強引用的時候被回收掉,這樣可以避免因為線程得不到銷毀導緻ThreadLocal對象無法被回收

hash沖突的解決

<code>&amp; (INITIAL_CAPACITY - 1)</code>,這是取模的一種方式,對于2的幂取模,用此代替<code>%(2^n)</code>,這也就是為啥容量必須為2的幂

<code>firstKey.threadLocalHashCode</code>:

這裡定義了一個AtomicInteger類型,每次擷取目前值并加上HASH_INCREMENT,<code>HASH_INCREMENT = 0x61c88647</code>,這個值是32位整型上限2^32-1乘以黃金分割比例0.618....的值2654435769,用有符号整型表示就是-1640531527,去掉符号後16進制表示為0x61c88647,目的就是為了讓哈希碼能均勻的分布在2的n次方的數組<code>Entry[] table</code>中。

線性探測法:

該方法一次探測下一個位址,直到有空的位址後插入,若整個空間都找不到空餘的位址,則産生溢出。假設目前table長度為16,也就是說如果計算出來key的hash值為14,如果table[14]上已經有值,并且其key與目前key不一緻,那麼就發生了hash沖突,這個時候将14加1得到15,取table[15]進行判斷,這個時候如果還是沖突會回到0,取table[0],以此類推,直到可以插入。<code>可以把table看成一個環形數組</code>

ThreadLocalMap的set():