說起CS遊戲,應該是每個中二少年的年少回憶了.
遊戲開始時,每個人能夠領到一把槍,槍把上有三個數字:子彈數、殺敵數、自己的命數,為其設定的初始值分别為1500、0、10.
設戰場上的每個人都是一個線程,那麼這三個初始值寫在哪裡呢?
如果每個線程都寫死這三個值,萬一将初始子彈數統一改成 1000發呢?
如果共享,那麼線程之間的并發修改會導緻資料不準确.
能不能構造這樣一個對象,将這個對象設定為共享變量,統一設定初始值,但是每個線程對這個值的修改都是互相獨立的.這個對象就是ThreadLocal
注意不能将其翻譯為線程本地化或本地線程
英語恰當的名稱應該叫作:CopyValueIntoEveryThread
示例代碼
該示例中,無 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.
在第1處,使用了
CsGameByThreadLocal
生成單獨的
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()
=========================== 增添内容 ==============================
線程封閉
避免并發異常最簡單的方法就是線程封閉
即 把對象封裝到一個線程裡,隻有該線程能看到此對象;
那麼該對象就算非線程安全,也不會出現任何并發安全問題.
ThreadLocal
是實作線程封閉的最佳實踐.
ThreadLocal
内部維護了一個Map,Map的key是每個線程的名稱,Map的值就是我們要封閉的對象.
每個線程中的對象都對應着Map中一個值,也就是
ThreadLocal
利用Map實作了對象的線程封閉.
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接口
-
中,也是初始化一個大小為16的Entry數組ThreadLoalMap
- Entry節點對象用來儲存每一個key-value鍵值對
這裡的key 恒為 ThreadLocal;
通過
ThreadLocal
set()
,把
ThreadLocal
對象自身當做key,放進
ThreadLoalMap

ThreadLoalMap
Entry
繼承
WeakReference
和HashMap很不同,
Entry
中沒有
next
字段,是以不存在連結清單情形.
hash沖突
無連結清單,那發生hash沖突時何解?
先看看
ThreadLoalMap
插入一個 key/value 的實作
- 每個
對象都有一個hash值 -ThreadLocal
threadLocalHashCode
- 每初始化一個
對象,hash值就增加一個固定大小ThreadLocal
在插入過程中,根據
ThreadLocal
對象的hash值,定位至table中的位置i.
過程如下
- 若目前位置為空,就初始化一個Entry對象置于i;
- 位置i已有對象
- 若該
對象的key正是将設定的key,覆寫其value(和HashMap 處理相同);Entry
- 若和即将設定的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
類對象;
可以将線程自己的對象保持到其中,各管各的,線程可以正确的通路到自己的對象.
- 将一個共用的
靜态執行個體作為key,将不同對象的引用儲存到不同線程的ThreadLocalMap中,然後線上程執行的各處通過這個靜态ThreadLocal執行個體的get()方法取得自己線程儲存的那個對象,避免了将這個對象作為參數傳遞的麻煩.ThreadLocal
當然如果要把本來線程共享的對象通過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開發手冊》