1 線程封閉
多線程訪問共享可變數據時,涉及到線程間數據同步的問題。並不是所有時候,都要用到
共享數據,所以線程封閉概念就提出來了。
數據都被封閉在各自的線程之中,就不需要同步,這種通過將數據封閉在線程中而避免使
用同步的技術稱爲線程封閉。
避免併發異常最簡單的方法就是線程封閉
即 把對象封裝到一個線程裏,只有該線程能看到此對象;
那麼該對象就算非線程安全,也不會出現任何併發安全問題.
1.1 棧封閉
局部變量的固有屬性之一就是封閉在線程中。
它們位於執行線程的棧中,其他線程無法訪問這個棧
1.2 使用ThreadLocal
是實現線程封閉的最佳實踐.
ThreadLocal是Java裏一種特殊的變量。
它是一個線程級變量,每個線程都有一個ThreadLocal, 就是每個線程都擁有了自己獨立的一個變量,
競爭條件被徹底消除了,在併發模式下是絕對安全的變量。
- 用法
ThreadLocal<T> var = new ThreadLocal<T>();
會自動在每一個線程上創建一個T的副本,副本之間彼此獨立,互不影響。
可以用 ThreadLocal 存儲一些參數, 以便在線程中多個方法中使用,用來代替方法傳參的做法。
實例
ThreadLocal
內部維護了一個Map,Map的key是每個線程的名稱,Map的value就是我們要封閉的對象.
每個線程中的對象都對應着Map中一個值,也就是ThreadLocal
利用Map實現了對象的線程封閉.
對於CS遊戲,開始時,每個人能夠領到一把槍,槍把上有三個數字:子彈數、殺敵數、自己的命數,爲其設置的初始值分別爲1500、0、10.
設戰場上的每個人都是一個線程,那麼這三個初始值寫在哪裏呢?如果每個線程都寫死這三個值,萬一將初始子彈數統一改成 1000發呢?
如果共享,那麼線程之間的併發修改會導致數據不準確.
能不能構造這樣一個對象,將這個對象設置爲共享變量, 統一設置初始值, 但是每個線程對這個值的修改都是互相獨立的.這個對象就是ThreadLocal
注意不能將其翻譯爲線程本地化或本地線程
英語恰當的名稱應該叫作:CopyValueIntoEveryThread
- 示例代碼
實在難以理解的,可以理解爲,JVM維護了一個Map<Thread, T>,每個線程要用這個T的時候,用當前的線程去Map裏面取。僅作爲一個概念理解
該示例中,無 set 操作,那麼初始值又是如何進入每個線程成爲獨立拷貝的呢?
首先,雖然ThreadLocal
在定義時重寫了initialValue()
,但並非是在BULLET_ NUMBER_ THREADLOCAL
對象加載靜態變量的時候執行;
而是每個線程在ThreadLocal.get()
時都會執行到;
-
其源碼如下
每個線程都有自己的ThreadLocalMap
;
如果map ==null
,則直接執行setInitialValue()
;
如果 map 已創建,就表示 Thread 類的threadLocals
屬性已初始化完畢;
如果e==null
,依然會執行到setinitialValue()
-
setinitialValue()
的源碼如下:
這是一個保護方法,CsGameByThreadLocal中初始化ThreadLocal對象時已覆寫value = initialValue()
getMap
的源碼就是提取線程對象t的ThreadLocalMap屬性: t. threadLocals.
在
CsGameByThreadLocal
第1處,使用了ThreadLocalRandom
生成單獨的Random
實例;
該類在JDK7中引入,它使得每個線程都可以有自己的隨機數生成器;
我們要避免Random
實例被多線程使用,雖然共享該實例是線程安全的,但會因競爭同一seed
而導致性能下降.
我們已經知道了ThreadLocal
是每一個線程單獨持有的;
因爲每一個線程都有獨立的變量副本,其他線程不能訪問,所以不存在線程安全問題,也不會影響程序的執行性能.
ThreadLocal
對象通常是由private static
修飾的,因爲都需要複製到本地線程,所以非static
作用不大;
不過,ThreadLocal
無法解決共享對象的更新問題,下面的實例將證明這點.
因爲CsGameByThreadLocal
中使用的是Integer
不可變對象,所以可使用相同的編碼方式來操作一下可變對象看看
輸出的結果是亂序不可控的,所以使用某個引用來操作共享對象時,依然需要進行線程同步
ThreadLocal
有個靜態內部類ThreadLocalMap
,它還有一個靜態內部類Entry
;
在Thread中的ThreadLocalMap
屬性的賦值是在ThreadLocal
類中的createMap
.
ThreadLocal
與ThreadLocalMap
有三組對應的方法: get()、set()和remove();
在ThreadLocal
中對它們只做校驗和判斷,最終的實現會落在ThreadLocalMap.
.
Entry
繼承自WeakReference
,只有一個value成員變量,它的key是ThreadLocal對象
再從棧與堆的內存角度看看兩者的關係
一個Thread有且僅有一個ThreadLocalMap
對象
一個Entry
對象的 key 弱引用指向一個ThreadLocal
對象
一個ThreadLocalMap
對象存儲多個Entry 對象
一個ThreadLocal
對象可被多個線程共享
ThreadLocal
對象不持有Value,Value 由線程的Entry 對象持有.
Entry 對象源碼如下
所有的Entry
對象都被ThreadLocalMap
類實例化對象threadLocals
持有;
當線程執行完畢時,線程內的實例屬性均會被垃圾回收,弱引用的ThreadLocal
,即使線程正在執行,只要ThreadLocal
對象引用被置成null
,Entry
的Key就會自動在下一次Y - GC時被垃圾回收;
而在ThreadLocal
使用set()/get()
時,又會自動將那些key=null
的value 置爲null
,使value能夠被GC,避免內存泄漏,現實很骨感, ThreadLocal如源碼註釋所述:
ThreadLocal
對象通常作爲私有靜態變量使用,那麼其生命週期至少不會隨着線程結束而結束.
三個重要方法:
- set()
如果沒有set操作的ThreadLocal
, 很容易引起髒數據問題 - get()
始終沒有get操作的ThreadLocal
對象是沒有意義的 - remove()
如果沒有remove操作,則容易引起內存泄漏
如果ThreadLocal
是非靜態的,屬於某個線程實例,那就失去了線程間共享的本質屬性;
那麼ThreadLocal
到底有什麼作用呢?
我們知道,局部變量在方法內各個代碼塊間進行傳遞,而類變量在類內方法間進行傳遞;
複雜的線程方法可能需要調用很多方法來實現某個功能,這時候用什麼來傳遞線程內變量呢?
即ThreadLocal
,它通常用於同一個線程內,跨類、跨方法傳遞數據;
如果沒有ThreadLocal,那麼相互之間的信息傳遞,勢必要靠返回值和參數,這樣無形之中,有些類甚至有些框架會互相耦合;
通過將Thread構造方法的最後一個參數設置爲true,可以把當前線程的變量繼續往下傳遞給它創建的子線程
public Thread (ThreadGroup group, Runnable target, String name,long stackSize, boolean inheritThreadLocals) [
this (group, target, name, stackSize, null, inheritThreadLocals) ;
}
parent爲其父線程
if (inheritThreadLocals && parent. inheritableThreadLocals != null)
this. inheritableThreadLocals = ThreadLocal. createInheritedMap (parent. inheritableThreadLocals) ;
createlnheritedMap()
其實就是調用ThreadLocalMap
的私有構造方法來產生一個實例對象,把父線程中不爲null
的線程變量都拷貝過來
private ThreadLocalMap (ThreadLocalMap parentMap) {
// table就是存儲
Entry[] parentTable = parentMap. table;
int len = parentTable. length;
setThreshold(len) ;
table = new Entry[len];
for (Entry e : parentTable) {
if (e != null) {
ThreadLocal<object> key = (ThreadLocal<object>) e.get() ;
if (key != null) {
object value = key. childValue(e.value) ;
Entry c = new Entry(key, value) ;
int h = key. threadLocalHashCode & (len - 1) ;
while (table[h] != null)
h = nextIndex(h, len) ;
table[h] = C;
size++;
}
}
}
很多場景下可通過ThreadLocal
來透傳全局上下文的;
比如用ThreadLocal
來存儲監控系統的某個標記位,暫且命名爲traceld.
某次請求下所有的traceld都是一致的,以獲得可以統一解析的日誌文件;
但在實際開發過程中,發現子線程裏的traceld爲null,跟主線程的traceld並不一致,所以這就需要剛纔說到的InheritableThreadLocal
來解決父子線程之間共享線程變量的問題,使整個連接過程中的traceld一致.
示例代碼如下
import org.apache.commons.lang3.StringUtils;
/**
* @author sss
* @date 2019/1/17
*/
public class RequestProcessTrace {
private static final InheritableThreadLocal<FullLinkContext> FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL
= new InheritableThreadLocal<FullLinkContext>();
public static FullLinkContext getContext() {
FullLinkContext fullLinkContext = FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.get();
if (fullLinkContext == null) {
FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.set(new FullLinkContext());
fullLinkContext = FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.get();
}
return fullLinkContext;
}
private static class FullLinkContext {
private String traceId;
public String getTraceId() {
if (StringUtils.isEmpty(traceId)) {
FrameWork.startTrace(null, "JavaEdge");
traceId = FrameWork.getTraceId();
}
return traceId;
}
public void setTraceId(String traceId) {
this.traceId = traceId;
}
}
}
使用ThreadLocal
和InheritableThreadLocal
透傳上下文時,需要注意線程間切換、異常傳輸時的處理,避免在傳輸過程中因處理不當而導致的上下文丟失.
最後,SimpleDateFormat
是非線程安全的類,定義爲static,會有數據同步風險.
通過源碼可以看出,SimpleDateFormat
內部有一個Calendar
對象;
在日期轉字符串或字符串轉日期的過程中,多線程共享時很可能產生錯誤;
推薦使用ThreadLocal
,讓每個線程單獨擁有這個對象.
ThreadLocal的副作用
爲了使線程安全地共享某個變量,JDK給出了ThreadLocal
.
但ThreadLocal
的主要問題是會產生髒數據和內存泄漏;
這兩個問題通常是在線程池的線程中使用ThreadLocal引發的,因爲線程池有線程複用和內存常駐兩是在線程池的線程中使用ThreadLocal 引發的,因爲線程池有線程複用和內存常駐兩個特點
1 髒數據
線程複用會產生髒數據;
由於線程池會重用 Thread
對象,與 Thread
綁定的靜態屬性 ThreadLoca
l變量也會被重用.
如果在實現的線程run()方法中不顯式調用remove()
清理與線程相關的ThreadLocal
信息,那麼若下一個線程不調用set()
,就可能get()
到重用的線程信息;
包括ThreadLocal
所關聯的線程對象的value值.
髒讀問題其實十分常見.
比如,用戶A下單後沒有看到訂單記錄,而用戶B卻看到了用戶A的訂單記錄.
通過排查發現是由於 session 優化引發.
在原來的請求過程中,用戶每次請求Server,都需要通過 sessionId 去緩存裏查詢用戶的session信息,這樣無疑增加了一次調用.
因此,工程師決定採用某框架來緩存每個用戶對應的SecurityContext
, 它封裝了session 相關信息.
優化後雖然會爲每個用戶新建一個 session 相關的上下文,但由於Threadlocal
沒有在線程處理結束時及時remove()
;
在高併發場景下,線程池中的線程可能會讀取到上一個線程緩存的用戶信息.
- 示例代碼
2 內存泄漏
在源碼註釋中提示使用static關鍵字來修飾ThreadLocal
.
在此場景下,寄希望於ThreadLocal
對象失去引用後,觸發弱引用機制來回收Entry
的Value
就不現實了.
在上例中,如果不進行remove()
,那麼當該線程執行完成後,通過ThreadLocal
對象持有的String對象是不會被釋放的.
- 以上兩個問題的解決辦法很簡單
每次用完ThreadLocal時,及時調用remove()
清理
What is ThreadLocal
該類提供了線程局部 (thread-local) 變量;
這些變量不同於它們的普通對應物,因爲訪問某變量(通過其 get /set 方法)的每個線程都有自己的局部變量,它獨立於變量的初始化副本.
ThreadLocal
實例通常是類中的 private static
字段,希望將狀態與某一個線程(e.g. 用戶 ID 或事務 ID)相關聯.
一個以ThreadLocal
對象爲鍵、任意對象爲值的存儲結構;
有點像HashMap
,可以保存"key : value"鍵值對,但一個ThreadLocal
只能保存一個鍵值對,各個線程的數據互不干擾.
該結構被附帶在線程上,也就是說一個線程可以根據一個ThreadLocal
對象查詢到綁定在這個線程上的一個值.
ThreadLocal<String> localName = new ThreadLocal();
localName.set("JavaEdge");
String name = localName.get();
在線程A中初始化了一個ThreadLocal對象localName,並set了一個值JavaEdge;
同時在線程A中通過get可拿到之前設置的值;
但是如果在線程B中,拿到的將是一個null.
因爲ThreadLocal
保證了各個線程的數據互不干擾
看看set(T value)和get()方法的源碼
可見,每個線程中都有一個ThreadLocalMap
- 執行set時,其值是保存在當前線程的
threadLocals
變量 - 執行get時,從當前線程的
threadLocals
變量獲取
所以在線程A中set的值,是線程B永遠得不到的
即使在線程B中重新set,也不會影響A中的值;
保證了線程之間不會相互干擾.
追尋本質 - 結構
從名字上看猜它類似HashMap,但在ThreadLocal
中,並無實現Map接口
-
在
ThreadLoalMap
中,也是初始化一個大小爲16的Entry數組
-
Entry節點對象用來保存每一個key-value鍵值對
這裏的key 恆爲 ThreadLocal;
通過ThreadLocal
的set()
,把ThreadLocal
對象自身當做key,放進ThreadLoalMap
ThreadLoalMap
的Entry
繼承WeakReference
和HashMap很不同,Entry
中沒有next
字段,所以不存在鏈表情形.
hash衝突
無鏈表,那發生hash衝突時何解?
先看看ThreadLoalMap
插入一個 key/value 的實現
- 每個
ThreadLocal
對象都有一個hash值 -threadLocalHashCode
- 每初始化一個
ThreadLocal
對象,hash值就增加一個固定大小
在插入過程中,根據ThreadLocal
對象的hash值,定位至table中的位置i.
過程如下
- 若當前位置爲空,就初始化一個Entry對象置於i;
- 位置i已有對象
- 若該
Entry
對象的key正是將設置的key,覆蓋其value(和HashMap 處理相同); - 若和即將設置的key 無關,則尋找下一個空位
- 若該
如此,在get
時,也會根據ThreadLocal
對象的hash值,定位到table中的位置.然後判斷該位置Entry對象中的key是否和get的key一致,如果不一致,就判斷下一個位置.
可見,set和get如果衝突嚴重的話,效率很低,因爲ThreadLoalMap
是Thread的一個屬性,所以即使在自己的代碼中控制了設置的元素個數,但還是不能控制其它代碼的行爲
內存泄露
ThreadLocal可能導致內存泄漏,爲什麼?
先看看Entry的實現:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
通過之前的分析已經知道,當使用ThreadLocal保存一個value時,會在ThreadLocalMap中的數組插入一個Entry對象,按理說key-value都應該以強引用保存在Entry對象中,但在ThreadLocalMap的實現中,key被保存到了WeakReference對象中
這就導致了一個問題,ThreadLocal在沒有外部強引用時,發生GC時會被回收,如果創建ThreadLocal的線程一直持續運行,那麼這個Entry對象中的value就有可能一直得不到回收,發生內存泄露。
避免內存泄露
既然發現有內存泄露的隱患,自然有應對策略,在調用ThreadLocal的get()、set()可能會清除ThreadLocalMap中key爲null的Entry對象,這樣對應的value就沒有GC Roots可達了,下次GC的時候就可以被回收,當然如果調用remove方法,肯定會刪除對應的Entry對象。
如果使用ThreadLocal的set方法之後,沒有顯示的調用remove方法,就有可能發生內存泄露,所以養成良好的編程習慣十分重要,使用完ThreadLocal之後,記得調用remove方法。
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("JavaEdge");
// 其它業務邏輯
} finally {
localName.remove();
}
題外小話
首先,ThreadLocal 不是用來解決共享對象的多線程訪問問題的.
一般情況下,通過set() 到線程中的對象是該線程自己使用的對象,其他線程是不需要訪問的,也訪問不到的;
各個線程中訪問的是不同的對象.
另外,說ThreadLocal使得各線程能夠保持各自獨立的一個對象;
並不是通過set()實現的,而是通過每個線程中的new 對象的操作來創建的對象,每個線程創建一個,不是什麼對象的拷貝或副本。
通過set()將這個新創建的對象的引用保存到各線程的自己的一個map中,每個線程都有這樣一個map;
執行get()時,各線程從自己的map中取出放進去的對象,因此取出來的是各自線程中的對象.
ThreadLocal實例是作爲map的key來使用的.
如果set()進去的東西本來就是多個線程共享的同一個對象;
那麼多個線程的get()取得的還是這個共享對象本身,還是有併發訪問問題。
Hibernate中典型的 ThreadLocal 應用
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
首先判斷當前線程中有沒有放入 session,如果還沒有,那麼通過sessionFactory().openSession()
來創建一個session;
再將session set()
到線程中,實際是放到當前線程的ThreadLocalMap
;
這時,對於該 session 的唯一引用就是當前線程中的那個ThreadLocalMap;
threadSession 作爲這個值的key,要取得這個 session 可以通過threadSession.get();
裏面執行的操作實際是先取得當前線程中的ThreadLocalMap;
然後將threadSession作爲key將對應的值取出.
這個 session 相當於線程的私有變量,而不是public的.
顯然,其他線程中是取不到這個session的,他們也只能取到自己的ThreadLocalMap中的東西。要是session是多個線程共享使用的,那還不亂套了.
如果不用ThreadLocal怎麼實現呢?
可能就要在action中創建session,然後把session一個個傳到service和dao中,這可夠麻煩的;
或者可以自己定義一個靜態的map,將當前thread作爲key,創建的session作爲值,put到map中,應該也行,這也是一般人的想法.
但事實上,ThreadLocal的實現剛好相反,它是在每個線程中有一個map,而將ThreadLocal實例作爲key,這樣每個map中的項數很少,而且當線程銷燬時相應的東西也一起銷燬了
總之,ThreadLocal
不是用來解決對象共享訪問問題的;
而主要是提供了保持對象的方法和避免參數傳遞的方便的對象訪問方式
- 每個線程中都有一個自己的
ThreadLocalMap
類對象;
可以將線程自己的對象保持到其中,各管各的,線程可以正確的訪問到自己的對象. - 將一個共用的
ThreadLocal
靜態實例作爲key,將不同對象的引用保存到不同線程的ThreadLocalMap中,然後在線程執行的各處通過這個靜態ThreadLocal實例的get()方法取得自己線程保存的那個對象,避免了將這個對象作爲參數傳遞的麻煩.
當然如果要把本來線程共享的對象通過set()放到線程中也可以,可以實現避免參數傳遞的訪問方式;
但是要注意get()到的是那同一個共享對象,併發訪問問題要靠其他手段來解決;
但一般來說線程共享的對象通過設置爲某類的靜態變量就可以實現方便的訪問了,似乎沒必要放到線程中
ThreadLocal的應用場合
我覺得最適合的是按線程多實例(每個線程對應一個實例)的對象的訪問,並且這個對象很多地方都要用到。
可以看到ThreadLocal類中的變量只有這3個int型:
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
而作爲ThreadLocal實例的變量只有 threadLocalHashCode
nextHashCode 和HASH_INCREMENT 是ThreadLocal類的靜態變量
實際上
- HASH_INCREMENT是一個常量,表示了連續分配的兩個ThreadLocal實例的threadLocalHashCode值的增量
- nextHashCode 表示了即將分配的下一個ThreadLocal實例的threadLocalHashCode 的值
看一下創建一個ThreadLocal實例即new ThreadLocal()時做了哪些操作,構造方法ThreadLocal()
裏什麼操作都沒有,唯一的操作是這句
private final int threadLocalHashCode = nextHashCode();
那麼nextHashCode()做了什麼呢
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
就是將ThreadLocal類的下一個hashCode值即nextHashCode的值賦給實例的threadLocalHashCode,然後nextHashCode的值增加HASH_INCREMENT這個值。.
因此ThreadLocal實例的變量只有這個threadLocalHashCode,而且是final的,用來區分不同的ThreadLocal實例;
ThreadLocal類主要是作爲工具類來使用,那麼set()進去的對象是放在哪兒的呢?
看一下上面的set()方法,兩句合併一下成爲
ThreadLocalMap map = Thread.currentThread().threadLocals;
這個ThreadLocalMap 類是ThreadLocal中定義的內部類,但是它的實例卻用在Thread類中:
public class Thread implements Runnable {
......
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
......
}
再看這句:
if (map != null)
map.set(this, value);
也就是將該ThreadLocal實例作爲key,要保持的對象作爲值,設置到當前線程的ThreadLocalMap 中,get()方法同樣看了代碼也就明白了.
參考
《碼出高效:Java開發手冊》