Java 併發編程 ThreadLocal

​ThreadLocal源碼分析  線程變量透傳  如何避免髒數據  內存溢出

2014年拍攝於甘南藏族自治區桑科草原,喜歡陰天

微信公衆號

王皓的GitHub:https://github.com/TenaciousDWang

 

今天這回說一下ThreadLocal這個類。

 

ThreadLocal爲變量在每個線程中都創建了一個獨立副本,那麼每個線程可以訪問自己內部的副本變量,其他線程不能訪問,所以不會產生線程安全問題,但是ThreadLocal無法解決共享變量更新問題,依然需要線程同步。

 

這裏引用《碼出高效》裏面CS的例子進行改造來說一下ThreadLocal的使用方法和場景。

 

在CS中每人對應三個數值,子彈數,殺敵數,自己的血量,初始值分別爲1500,0,10.假設每個人屬於一個線程,這三個初始值寫在什麼地方呢?每一個線程寫死的話,如果同意修改子彈數量了呢?如果共享,由於每個人,每個線程對數據操作不同會導致數據不準確,這時我們就可以使用ThreadLocal對象,設置爲共享變量,統一設置初始值,但是每個線程都有一個副本是獨立的,對其他線程不可見的,《碼出高效》中對於ThreadLocal的名稱解釋爲CopyValueIntoEveryThread就顯得非常貼切。

 

 

首先我們看一下ThreadLocal創建時通常都是使用private static 來修飾的。

 

代碼示例中並沒有對ThreadLocal進行set操作,而是重寫了initialValue方法返回一個共享變量,我們來看一下ThreadLocal的get操作的源碼。

 

 

源碼中,每一個Thread都包含一個ThreadLocalMap,ThreadLocal的get方法實際上是從這個Map中獲取Entry,再從Entry中獲取當前副本的值,如果無法獲取,調用setInitialValue方法。

 

 

其中如果當前線程沒有ThreadLocalMap就創建,有的話會set進一個初始鍵ThreadLocal<T>對應值爲initialValue方法返回,這是一個保護方法。

 

 

這裏我們之前重寫了initialValue方法不返回null,而是返回了我們定義的共享變量。

 

接下來我們在線程的run方法內寫入ThreadLocal變量減去隨機的int值,並打印每一個線程對應的三個值的結果。這裏爲了結果容易分辨,我加了一個鎖,讓其按順序打印。

 

 

我們可以看一下打印的結果,每一個線程的副本變量都只對自己可見,對其他線程不可見,打印的都是線程自己對自己副本變量操作的結果,線程間不會互相干擾,可以看出是線程安全的。

 

以上是每個線程對自己的變量副本進行操作,如果是多線程對共享變量進行更新則ThreadLocal無法解決,我們依舊需要線程同步,例子如下,將變量改爲可變對象StringBuilder,注意其兄弟StringBuffer是線程安全的。

 

 

此時sb這個ThreadLocal是一個StringBuilder線程不安全的可變對象,我們起十個線程來對其添加-i,我們來看一下運行結果。

 

 

我們看到開始打印爲正常順序,到後面時,已經變爲亂序,因此當有多線程引用共享變量時,依舊需要線程同步的,ThreadLocal並不能解決共享變量多線程操作的安全問題。

 

接下來看一下ThreadLocal使用中需要注意的髒數據,內存泄漏問題。

 

A、髒數據,我們知道線程池屬於複用線程,也就是說線程中的ThreadLocal也會被複用,如果下一個線程沒有set初始值,那麼就有可能get到上一下線程用過的信息,或者上一個線程沒有進行ThreadLocal的remove操作。

 

B、內存泄漏。

 

 

源碼註釋中寫道,ThreadLocal對象通常最爲私有靜態變量使用,其生命週期不會隨着線程結束而結束,所以必須及時調用remove方法來清理,避免內存泄漏。

 

總結上述線程使用ThreadLocal三個重要方法需要注意的問題:

 

1、set:如果沒有set操作ThreadLocal,容易引起髒數據問題。

 

2、get:始終沒有get操作的ThreadLocal是沒有意義的。

 

3、remove:如果沒有remove操作,容易引起內存泄漏問題。

 

最後說一個ThreadLocal子類InheritableThreadLocal用於子線程共享父線程本地變量的功能,也就是線程透傳功能。

 

舉一個例子,先來看一下基本使用方法與場景。

 

 

main爲父線程,創建InheritableThreadLocal,然後在父線程中創建子線程threadTest,我們看一下運行結果。

 

 

可以獲得父線程內變量的值。爲什麼能獲取到呢,我們來看一下InheritableThreadLocal的源碼來分析一下。

 

 

InheritableThreadLocal繼承自ThreadLocal,並且重寫了父類的方法:createMap,getMap,childValue。createMap中使用了Thread中的inheritableThreadLocals我們來看一下它會被怎麼賦值。

 

 

會把parent也就是父線程,使用createInheritedMap方法來把父線程的inheritableThreadLocals中不是null的線程變量都copy過來。

 

 

所以子線程可以獲取到父線程的線程內變量。但是InheritableThreadLocal其實在使用線程池時也存在髒數據的問題,目前的解決方案是使用alibaba的開源項目transmittable-thread-local來解決這個問題。大家可以上github上自行查看使用說明,寫的很詳細。

 

最後說一個是關於SimpleDateFormat,是一個線程不安全的類,定義爲static會有同步風險,多線程共享時會產生錯誤。《碼出高效》推薦使用ThreadLocal來解決。

 

 

好了,這就是今天要說Java併發編程ThreadLocal,喜歡請關注~

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章