1. 程式人生 > 實用技巧 >面試官,ThreadLocal 你別再問了!

面試官,ThreadLocal 你別再問了!

來源:juejin.im/post/5d427f306fb9a06b122f1b94

ThreadLocal是什麼

以前面試的時候問到ThreadLocal總是一臉懵逼,只知道有這個哥們,不瞭解他是用來做什麼的,更不清楚他的原理了。表面上看他是和多執行緒,執行緒同步有關的一個工具類,但其實他與執行緒同步機制無關。執行緒同步機制是多個執行緒共享同一個變數,而ThreadLocal是為每個執行緒建立一個單獨的變數副本,每個執行緒都可以改變自己的變數副本而不影響其它執行緒所對應的副本。

官方API上是這樣介紹的:該類提供了執行緒區域性(thread-local)變數。這些變數不同於它們的普通對應物,因為訪問某個變數(通過其 get 或 set 方法)的每個執行緒都有自己的區域性變數,它獨立於變數的初始化副本。ThreadLocal例項通常是類中的 private static 欄位,它們希望將狀態與某一個執行緒(例如,使用者 ID 或事務 ID)相關聯。

ThreadLocal的API

ThreadLocal定義了四個方法:

  • get():返回此執行緒區域性變數當前副本中的值

  • set(T value):將執行緒區域性變數當前副本中的值設定為指定值

  • initialValue():返回此執行緒區域性變數當前副本中的初始值

  • remove():移除此執行緒區域性變數當前副本中的值

ThreadLocal還有一個特別重要的靜態內部類ThreadLocalMap,該類才是實現執行緒隔離機制的關鍵。get()、set()、remove()都是基於該內部類進行操作,ThreadLocalMap用鍵值對方式儲存每個執行緒變數的副本,key為當前的ThreadLocal物件,value為對應執行緒的變數副本。

試想,每個執行緒都有自己的ThreadLocal物件,也就是都有自己的ThreadLocalMap,對自己的ThreadLocalMap操作,當然是互不影響的了,這就不存線上程安全問題了,所以ThreadLocal是以空間來交換安全性的解決思路。

使用例項

假設每個執行緒都需要一個計數值記錄自己做某件事做了多少次,各執行緒執行時都需要改變自己的計數值而且相互不影響,那麼ThreadLocal就是很好的選擇,這裡ThreadLocal裡儲存的當前執行緒的區域性變數的副本就是這個計數值。

publicclassSeqCount{

privatestaticThreadLocal<Integer>seqCount=newThreadLocal<Integer>(){
@Override
protectedIntegerinitialValue(){
return0;
}
};


publicintnextSeq(){
seqCount.set(seqCount.get()+1);
returnseqCount.get();
}

publicstaticvoidmain(String[]args){
SeqCountseqCount=newSeqCount();

SeqThreadseqThread1=newSeqThread(seqCount);
SeqThreadseqThread2=newSeqThread(seqCount);
SeqThreadseqThread3=newSeqThread(seqCount);
SeqThreadseqThread4=newSeqThread(seqCount);

seqThread1.start();
seqThread2.start();
seqThread3.start();
seqThread4.start();
}

publicstaticclassSeqThreadextendsThread{

privateSeqCountseqCount;

publicSeqThread(SeqCountseqCount){
this.seqCount=seqCount;
}

@Override
publicvoidrun(){
for(inti=0;i<3;i++){
System.out.println(Thread.currentThread().getName()+"seqCount:"+seqCount.nextSeq());
}
}
}
}

執行結果:

解決SimpleDateFormat的執行緒安全

我們知道SimpleDateFormat在多執行緒下是存線上程安全問題的,那麼將SimpleDateFormat作為每個執行緒的區域性變數的副本就是每個執行緒都擁有自己的SimpleDateFormat,就不存線上程安全問題了。

publicclassSimpleDateFormatDemo{

privatestaticfinalStringDATE_FORMAT="yyyy-MM-ddHH:mm:ss";

privatestaticThreadLocal<DateFormat>threadLocal=newThreadLocal<>();

/**
*獲取執行緒的變數副本,如果不覆蓋initialValue方法,第一次get將返回null,故需要建立一個DateFormat,放入threadLocal中
*@return
*/
publicDateFormatgetDateFormat(){
DateFormatdf=threadLocal.get();
if(df==null){
df=newSimpleDateFormat(DATE_FORMAT);
threadLocal.set(df);
}
returndf;
}

publicstaticvoidmain(String[]args){
SimpleDateFormatDemoformatDemo=newSimpleDateFormatDemo();

MyRunnablemyRunnable1=newMyRunnable(formatDemo);
MyRunnablemyRunnable2=newMyRunnable(formatDemo);
MyRunnablemyRunnable3=newMyRunnable(formatDemo);

Threadthread1=newThread(myRunnable1);
Threadthread2=newThread(myRunnable2);
Threadthread3=newThread(myRunnable3);
thread1.start();
thread2.start();
thread3.start();
}


publicstaticclassMyRunnableimplementsRunnable{

privateSimpleDateFormatDemodateFormatDemo;

publicMyRunnable(SimpleDateFormatDemodateFormatDemo){
this.dateFormatDemo=dateFormatDemo;
}

@Override
publicvoidrun(){
System.out.println(Thread.currentThread().getName()+"當前時間:"+dateFormatDemo.getDateFormat().format(newDate()));
}
}
}

執行結果:

原始碼分析

ThreadLocalMap

ThreadLocalMap內部是利用Entry來進行key-value的儲存的。

staticclassEntryextendsWeakReference<ThreadLocal<?>>{
/**ThevalueassociatedwiththisThreadLocal.*/
Objectvalue;

Entry(ThreadLocal<?>k,Objectv){
super(k);
value=v;
}
}

上面原始碼中key就是ThreadLocal,value就是值,Entry繼承WeakReference,所以Entry對應key的引用(ThreadLocal例項)是一個弱引用。

set(ThreadLocal key, Object value)

/**
*Setthevalueassociatedwithkey.
*
*@paramkeythethreadlocalobject
*@paramvaluethevaluetobeset
*/
privatevoidset(ThreadLocal<?>key,Objectvalue){
Entry[]tab=table;
intlen=tab.length;
//根據ThreadLocal的雜湊值,查詢對應元素在陣列中的位置
inti=key.threadLocalHashCode&(len-1);
//採用線性探測法尋找合適位置
for(Entrye=tab[i];e!=null;e=tab[i=nextIndex(i,len)]){
ThreadLocal<?>k=e.get();
//key存在,直接覆蓋
if(k==key){
e.value=value;
return;
}
//key==null,但是存在值(因為此處的e!=null),說明之前的ThreadLocal物件已經被回收了
if(k==null){
replaceStaleEntry(key,value,i);
return;
}
}
//ThreadLocal對應的key例項不存在,new一個
tab[i]=newEntry(key,value);
intsz=++size;
//清楚陳舊的Entry(key==null的)
//如果沒有清理陳舊的Entry並且陣列中的元素大於了閾值,則進行rehash
if(!cleanSomeSlots(i,sz)&&sz>=threshold)
rehash();
}

這個set操作和集合Map解決雜湊衝突的方法不同,集合Map採用的是鏈地址法,這裡採用的是開放定址法(線性探測)。set()方法中的replaceStaleEntry()和cleanSomeSlots(),這兩個方法可以清除掉key ==null的例項,防止記憶體洩漏。

getEntry()

privateEntrygetEntry(ThreadLocal<?>key){
inti=key.threadLocalHashCode&(table.length-1);
Entrye=table[i];
if(e!=null&&e.get()==key)
returne;
else
returngetEntryAfterMiss(key,i,e);
}

由於採用了開放定址法,當前keu的雜湊值和元素在陣列中的索引並不是一一對應的,首先取一個猜測數(key的雜湊值),如果所對應的key是我們要找的元素,那麼直接返回,否則呼叫getEntryAfterMiss

privateEntrygetEntryAfterMiss(ThreadLocal<?>key,inti,Entrye){
Entry[]tab=table;
intlen=tab.length;

while(e!=null){
ThreadLocal<?>k=e.get();
if(k==key)
returne;
if(k==null)
expungeStaleEntry(i);
else
i=nextIndex(i,len);
e=tab[i];
}
returnnull;
}

這裡一直在探測尋找下一個元素,知道找的元素的key是我們要找的。這裡當key==null時,呼叫expungeStaleEntry有利於GC的回收,用於防止記憶體洩漏。

ThreadLocal為什麼會記憶體洩漏

ThreadLocalMap的key為ThreadLocal例項,他是一個弱引用,我們知道弱引用有利於GC的回收,當key == null時,GC就會回收這部分空間,但value不一定能被回收,因為他和Current Thread之間還存在一個強引用的關係。由於這個強引用的關係,會導致value無法回收,如果執行緒物件不消除這個強引用的關係,就可能會出現OOM。有些時候,我們呼叫ThreadLocalMap的remove()方法進行顯式處理。

總結

ThreadLocal不是用來解決共享變數的問題,也不是協調執行緒同步,他是為了方便各執行緒管理自己的狀態而引用的一個機制。

每個ThreadLocal內部都有一個ThreadLocalMap,他儲存的key是ThreadLocal的例項,他的值是當前執行緒的區域性變數的副本的值。