天天看點

ThreadLocal學習

ThreadLocal學習

官方介紹:

ThreadLocal類用來提供線程内部的局部變量。這種變量在多線程環境下通路(通過get和set方法通路)時能保證各個線程的變量相對獨立于其他線程内的變量。ThreadLocal執行個體通常來說都是private static類型的,用于關聯線程和線程上下文。

總結: 在多線程并發的場景下使用 可以通過ThreadLocal在同一線程,不同元件中傳遞公共變量 每個線程的變量都是獨立的,不會互相影響(線程隔離)

常用方法

方法聲明

描述

ThreadLocal()

建立ThreadLocal對象

public void set( T value)

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

public T get()

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

public void remove()

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

可能的運作結果:

這是由于多個線程(0~4)公用一個變量(對象demo)導緻的異常,因為線程之間資料沒有隔離。

以上有兩種解決方法,一種方法是利用synchronized同步方式,另一種就是ThreadLocal類

運作結果:

從結果來看,确實可以解決問題,但是,在這裡們強調的是線程資料隔離的問題,并不是多線程共享資料的問題, 在這個案例中使用synchronized關鍵字是不合适的。

由上可知,建立了一個存儲String類型的ThreadLocal對象(tl),在setContent方法中使用ThreadLocal中的set()方法将content存進tl,getContent方法中使用ThreadLocal中的get()方法從tl中擷取content。

synchronized

ThreadLocal

原理

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

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

側重點

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

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

在案例中,雖然使用ThreadLocal和synchronized都能解決問題,但是使用ThreadLocal更為合适,因為這樣可以使程式擁有更高的并發性。

jdk8.0之前的結構:

ThreadLocal學習
每個<code>ThreadLocal</code>都建立一個<code>Map</code>,然後用線程作為<code>Map</code>的<code>key</code>,要存儲的局部變量作為<code>Map</code>的<code>value</code>,這樣就能達到各個線程的局部變量隔離的效果。這是最簡單的設計方法,JDK最早期的<code>ThreadLocal</code> 确實是這樣設計的,但現在早已不是了。

jdk8.0之後的結構:

ThreadLocal學習
每個Thread維護一個ThreadLocalMap,這個Map的key是ThreadLocal執行個體本身,value才是真正要存儲的值Object。

具體的過程是這樣的:

每個Thread線程内部都有一個Map (ThreadLocalMap)

Map裡面存儲ThreadLocal對象(key)和線程的變量副本(value)

Thread内部的Map是由ThreadLocal維護的,由ThreadLocal負責向map擷取和設定線程的變量值。

對于不同的線程,每次擷取副本值時,别的線程并不能擷取到目前線程的副本值,形成了副本的隔離,互不幹擾。

這樣設計之後每個Map存儲的Entry數量就會變少(降低哈希碰撞的機率)。因為之前的存儲數量由Thread的數量決定,現在是由ThreadLocal的數量決定。在實際運用當中,往往ThreadLocal的數量要少于Thread的數量。

當Thread銷毀之後,對應的ThreadLocalMap也會随之銷毀,能減少記憶體的使用。

set方法

上述代碼的執行流程:

首先擷取目前線程,并根據目前線程擷取其Map 如果擷取的Map不為空,則将參數設定到Map中(目前ThreadLocal的引用作為key) 如果Map為空,則給該線程建立 Map,并設定初始值

get方法

首先擷取目前線程, 根據目前線程擷取其Map 如果擷取的Map不為空,則在Map中以ThreadLocal的引用作為key來在Map中擷取對應的Entry ,否則轉到D 如果e不為null,則傳回e.value,否則轉到D Map為空或者e為空,則通過initialValue方法擷取值為初始值的value,然後用ThreadLocal的引用和value作為firstKey和firstValue建立一個新的Map

remove方法

上述代碼的執行流程

首先擷取目前線程,并根據目前線程擷取一個Map 如果擷取的Map不為空,則移除目前ThreadLocal對象對應的entry

initialValue方法

此方法的作用是 傳回該線程局部變量的初始值。 這個方法是一個延遲調用方法,從上面的代碼我們得知,在set方法還未調用而先調用了get方法時才執行,并且僅執行1次。 這個方法預設實作直接傳回一個null。 如果想要一個除null之外的初始值,可以重寫此方法。 (備注: 該方法是一個protected的方法,顯然是為了讓子類覆寫而設計的)

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

ThreadLocal學習
在ThreadLocalMap中,也是用Entry來儲存K-V結構資料的。不過Entry中的key隻能是ThreadLocal對象,這點在構造方法中已經限定死了。 另外,Entry繼承了WeakReference類,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal對象的生命周期和線程生命周期解綁。

Memory overflow:記憶體溢出,沒有足夠的記憶體提供申請者使用。

Memory leak: 記憶體洩漏是指程式中已動态配置設定的堆記憶體由于某種原因程式未釋放或無法釋放,造成系統記憶體的浪費,導緻程式運作速度減慢甚至系統崩潰等嚴重後果。記憶體洩漏的堆積終将導緻記憶體溢出。

Java中的引用有4種類型: 強、軟、弱、虛。目前這個問題主要涉及到強引用和弱引用:

強引用(“Strong” Reference),就是我們最常見的普通對象引用,隻要還有強引用指向一個對象,就能表明對象還“活着”,垃圾回收器就不會回收這種對象。

弱引用(WeakReference),垃圾回收器一旦發現了隻具有弱引用的對象,不管目前記憶體空間足夠與否,都會回收它的記憶體。

ThreadLocal學習
假設在業務代碼中使用完ThreadLocal ,ThreadLocalRef被回收了。 但是因為threadLocalMap的Entry強引用了ThreadLocal,造成ThreadLocal無法被回收。 在沒有手動删除這個Entry以及CurrentThread依然運作的前提下,始終有強引用鍊 CurrentThreadRef---&gt;CurrentThread-&gt;threadLocalMap---&gt;entry,Entry就不會被回收(Entry中包括了ThreadLocal執行個體和value),導緻Entry記憶體洩漏。 也就是說,ThreadLocalMap中的key使用了強引用, 是無法完全避免記憶體洩漏的。
ThreadLocal學習
同樣假設在業務代碼中使用完ThreadLocal ,ThreadLocalRef被回收了。由于ThreadLocalMap隻持有ThreadLocal的弱引用,沒有任何強引用指向ThreadLocal執行個體, 是以ThreadLocal就可以順利被gc,此時Entry中的key=null。 但是在沒有手動删除這個Entry以及CurrentThread依然運作的前提下,也存在有強引用鍊 CurrentThreadRef---&gt;CurrentThread-&gt;threadLocalMap---&gt;entry ---&gt; value ,value不會被回收, 而這塊value永遠不會被通路到了,導緻value記憶體洩漏。也就是說,ThreadLocalMap中的key使用了弱引用, 也有可能記憶體洩漏。

在以上兩種記憶體洩漏的情況中,都有兩個前提:

沒有手動删除這個Entry ThreadLocalRef結束後,CurrentThread依然運作

第一點很好了解,隻要在使用完ThreadLocal,調用其remove方法删除對應的Entry,就能避免記憶體洩漏。

第二點稍微複雜一點, 由于ThreadLocalMap是Thread的一個屬性,被目前線程所引用,是以它的生命周期跟Thread一樣長。那麼在使用完ThreadLocal之後,如果目前Thread也随之執行結束,ThreadLocalMap自然也會被gc回收,從根源上避免了記憶體洩漏。

綜上,ThreadLocal記憶體洩漏的根源是:

由于ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動删除對應key就會導緻記憶體洩漏。

事實上,在ThreadLocalMap中的set和getEntry方法中,會對key為null(也即是ThreadLocal為null)進行判斷,如果為null的話,那麼是會對value置為null的。

這就意味着使用完ThreadLocal,CurrentThread依然運作的前提下,就算忘記調用remove方法,弱引用比強引用可以多一層保障:弱引用的ThreadLocal會被回收,對應的value在下一次ThreadLocalMap調用set,get,remove中的任一方法的時候會被清除,進而避免記憶體洩漏。

根據剛才的分析, 我們知道了: 無論ThreadLocalMap中的key使用哪種類型引用都無法完全避免記憶體洩漏,跟使用弱引用沒有關系。

要避免記憶體洩漏有兩種方式:

使用完ThreadLocal,調用其remove方法删除對應的Entry 使用完ThreadLocal,目前Thread也随之運作結束

相對第一種方式,第二種方式顯然更不好控制,特别是使用線程池的時候,線程結束是不會銷毀的。

源碼:

重點: <code>int i = firstKey.threadLocalHashCode &amp; (INITIAL_CAPACITY - 1)</code>。

關于<code>firstKey.threadLocalHashCode</code>:

這裡定義了一個AtomicInteger類型,每次擷取目前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,這個值跟斐波那契數列(黃金分割數)有關,其主要目的就是為了讓哈希碼能均勻的分布在2的n次方的數組裡, 也就是Entry[] table中,這樣做可以盡量避免hash沖突。

關于<code>&amp; (INITIAL_CAPACITY - 1)</code>

計算hash的時候裡面采用了hashCode &amp; (size - 1)的算法,這相當于取模運算hashCode % size的一個更高效的實作。正是因為這種算法,我們要求size必須是2的整次幂,這也能保證在索引不越界的前提下,使得hash發生沖突的次數減小。

該方法一次探測下一個位址,直到有空的位址後插入,若整個空間都找不到空餘的位址,則産生溢出。

舉個例子,假設目前table長度為16,也就是說如果計算出來key的hash值為14,如果table[14]上已經有值,并且其key與目前key不一緻,那麼就發生了hash沖突,這個時候将14加1得到15,取table[15]進行判斷,這個時候如果還是沖突會回到0,取table[0],以此類推,直到可以插入。

按照上面的描述,可以把Entry[] table看成一個環形數組。

繼續閱讀