ThreadLocal的作用以及應用場景
ThreadLocal
算是一種并發容器吧,因為他的內部是有ThreadLocalMap
組成,ThreadLocal
是為了解決多線程情況下變量不能被共享的問題,也就是多線程共享變量的問題。
ThreadLocal
和Lock
以及Synchronized
的區別是:ThreadLocal
是給每個線程分配一個變量(對象),各個線程都存有變量的副本,這樣每個線程都是使用自己(變量)對象實例,使線程與線程之間進行隔離;而Lock
和Synchronized
的方式是使線程有順序的執行。
舉一個簡單的例子:目前有100個學生等待簽字,但是老師只有一個筆,那老師只能按順序的分給每個學生,等待A學生簽字完成然后將筆交給B學生,這就類似Lock
,Synchronized
的方式。而ThreadLocal
是,老師直接拿出一百個筆給每個學生;再效率提高的同事也要付出一個內存消耗;也就是以空間換時間的概念
基于 Spring Boot + MyBatis Plus + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
使用場景
Spring的事務隔離就是使用ThreadLocal
和AOP來解決的;主要是TransactionSynchronizationManager
這個類;
當我們使用SimpleDateFormat
的parse()
方法的時候,parse()
方法會先調用Calendar.clear()
方法,然后調用Calendar.add()
方法,如果一個線程先調用了add()
方法,然后另一個線程調用了clear()
方法;這時候parse()
方法就會出現解析錯誤;如果不信我們可以來個例子:
publicclassSimpleDateFormatTest{
privatestaticSimpleDateFormatsimpleDateFormat=newSimpleDateFormat("yyyy-MM-dd");
publicstaticvoidmain(String[]args){
for(inti=0;i50;i++){
Threadthread=newThread(newRunnable(){
@Override
publicvoidrun(){
dateFormat();
}
});
thread.start();
}
}
/**
*字符串轉成日期類型
*/
publicstaticvoiddateFormat(){
try{
simpleDateFormat.parse("2021-5-27");
}catch(ParseExceptione){
e.printStackTrace();
}
}
}
這里我們只啟動了50個線程問題就會出現,其實看巧不巧,有時候只有10個線程的情況就會出錯:
Exceptioninthread"Thread-40"java.lang.NumberFormatException:Forinputstring:""
atjava.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
atjava.lang.Long.parseLong(Long.java:601)
atjava.lang.Long.parseLong(Long.java:631)
atjava.text.DigitList.getLong(DigitList.java:195)
atjava.text.DecimalFormat.parse(DecimalFormat.java:2084)
atjava.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
atjava.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
atjava.text.DateFormat.parse(DateFormat.java:364)
atcn.haoxy.use.lock.sdf.SimpleDateFormatTest.dateFormat(SimpleDateFormatTest.java:36)
atcn.haoxy.use.lock.sdf.SimpleDateFormatTest$1.run(SimpleDateFormatTest.java:23)
atjava.lang.Thread.run(Thread.java:748)
Exceptioninthread"Thread-43"java.lang.NumberFormatException:multiplepoints
atsun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
atsun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
atjava.lang.Double.parseDouble(Double.java:538)
atjava.text.DigitList.getDouble(DigitList.java:169)
atjava.text.DecimalFormat.parse(DecimalFormat.java:2089)
atjava.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
atjava.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
atjava.text.DateFormat.parse(DateFormat.java:364)
at.............
其實解決這個問題很簡單,讓每個線程new一個自己的SimpleDateFormat
,但是如果100個線程都要new100個SimpleDateFormat
嗎?
當然我們不能這么做,我們可以借助線程池加上ThreadLocal
來解決這個問題:
publicclassSimpleDateFormatTest{
privatestaticThreadLocallocal=newThreadLocal(){
@Override
//初始化線程本地變量
protectedSimpleDateFormatinitialValue(){
returnnewSimpleDateFormat("yyyy-MM-dd");
}
};
publicstaticvoidmain(String[]args){
ExecutorServicees=Executors.newCachedThreadPool();
for(inti=0;i500;i++){
es.execute(()->{
//調用字符串轉成日期方法
dateFormat();
});
}
es.shutdown();
}
/**
*字符串轉成日期類型
*/
publicstaticvoiddateFormat(){
try{
//ThreadLocal中的get()方法
local.get().parse("2021-5-27");
}catch(ParseExceptione){
e.printStackTrace();
}
}
}
這樣就優雅的解決了線程安全問題;
解決過度傳參問題;例如一個方法中要調用好多個方法,每個方法都需要傳遞參數;例如下面示例:
voidwork(Useruser){
getInfo(user);
checkInfo(user);
setSomeThing(user);
log(user);
}
用了ThreadLocal
之后:
publicclassThreadLocalStu{
privatestaticThreadLocaluserThreadLocal=newThreadLocal<>();
voidwork(Useruser){
try{
userThreadLocal.set(user);
getInfo();
checkInfo();
someThing();
}finally{
userThreadLocal.remove();
}
}
voidsetInfo(){
Useru=userThreadLocal.get();
//.....
}
voidcheckInfo(){
Useru=userThreadLocal.get();
//....
}
voidsomeThing(){
Useru=userThreadLocal.get();
//....
}
}
每個線程內需要保存全局變量(比如在登錄成功后將用戶信息存到ThreadLocal
里,然后當前線程操作的業務邏輯直接get取就完事了,有效的避免的參數來回傳遞的麻煩之處),一定層級上減少代碼耦合度。
- 比如存儲 交易id等信息。每個線程私有。
- 比如aop里記錄日志需要before記錄請求id,end拿出請求id,這也可以。
-
比如jdbc連接池(很典型的一個
ThreadLocal
用法) - ....等等....
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
原理分析
上面我們基本上知道了ThreadLocal
的使用方式以及應用場景,當然應用場景不止這些這只是工作中常用到的場景;下面我們對它的原理進行分析;
我們先看一下它的set()
方法;
publicvoidset(Tvalue){
Threadt=Thread.currentThread();
ThreadLocalMapmap=getMap(t);
if(map!=null)
map.set(this,value);
else
createMap(t,value);
}
是不是特別簡單,首先獲取當前線程,用當前線程作為key,去獲取ThreadLocalMap
,然后判斷map是否為空,不為空就將當前線程作為key,傳入的value作為map的value值;如果為空就創建一個ThreadLocalMap
,然后將key和value方進去;從這里可以看出value值是存放到ThreadLocalMap
中;
然后我們看看ThreadLocalMap
是怎么來的?先看下getMap()
方法:
//在Thread類中維護了threadLocals變量,注意是Thread類
ThreadLocal.ThreadLocalMapthreadLocals=null;
//在ThreadLocal類中的getMap()方法
ThreadLocalMapgetMap(Threadt){
returnt.threadLocals;
}
這就能解釋每個線程中都有一個ThreadLocalMap
,因為ThreadLocalMap
的引用在Thread中維護;這就確保了線程間的隔離;
我們繼續回到set()
方法,看到當map等于空的時候createMap(t, value);
voidcreateMap(Threadt,TfirstValue){
t.threadLocals=newThreadLocalMap(this,firstValue);
}
這里就是new了一個ThreadLocalMap
然后賦值給threadLocals
成員變量;ThreadLocalMap
構造方法:
ThreadLocalMap(ThreadLocal>firstKey,ObjectfirstValue){
//初始化一個Entry
table=newEntry[INITIAL_CAPACITY];
//計算key應該存放的位置
inti=firstKey.threadLocalHashCode&(INITIAL_CAPACITY-1);
//將Entry放到指定位置
table[i]=newEntry(firstKey,firstValue);
size=1;
//設置數組的大小16*2/3=10,類似HashMap中的0.75*16=12
setThreshold(INITIAL_CAPACITY);
}
這里寫有個大概的印象,后面對ThreadLocalMap
內部結構還會進行詳細的講解;
下面我們再去看一下get()
方法:
publicTget(){
Threadt=Thread.currentThread();
//用當前線程作為key去獲取ThreadLocalMap
ThreadLocalMapmap=getMap(t);
if(map!=null){
//map不為空,然后獲取map中的Entry
ThreadLocalMap.Entrye=map.getEntry(this);
if(e!=null){
@SuppressWarnings("unchecked")
//如果Entry不為空就獲取對應的value值
Tresult=(T)e.value;
returnresult;
}
}
//如果map為空或者entry為空的話通過該方法初始化,并返回該方法的value
returnsetInitialValue();
}
get()
方法和set()
都比較容易理解,如果map等于空的時候或者entry等于空的時候我們看看setInitialValue()
方法做了什么事:
privateTsetInitialValue(){
//初始化變量值由子類去實現并初始化變量
Tvalue=initialValue();
Threadt=Thread.currentThread();
//這里再次getMap();
ThreadLocalMapmap=getMap(t);
if(map!=null)
map.set(this,value);
else
//和set()方法中的
createMap(t,value);
returnvalue;
}
下面我們再去看一下ThreadLocal
中的initialValue()
方法:
protectedTinitialValue(){
returnnull;
}
設置初始值,由子類去實現;就例如我們上面的例子,重寫ThreadLocal
類中的initialValue()
方法:
privatestaticThreadLocallocal=newThreadLocal(){
@Override
//初始化線程本地變量
protectedSimpleDateFormatinitialValue(){
returnnewSimpleDateFormat("yyyy-MM-dd");
}
};
createMap()
方法和上面set()
方法中createMap()
方法同一個,就不過多的敘述了;剩下還有一個removve()
方法
publicvoidremove(){
ThreadLocalMapm=getMap(Thread.currentThread());
if(m!=null)
//2.從map中刪除以當前threadLocal實例為key的鍵值對
m.remove(this);
}
源碼的講解就到這里,也都比較好理解,下面我們看看ThreadLocalMap
的底層結構
ThreadLocalMap的底層結構
上面我們已經了解了ThreadLocal
的使用場景以及它比較重要的幾個方法;下面我們再去它的內部結構;經過上的源碼分析我們可以看到數據其實都是存放到了ThreadLocal
中的內部類ThreadLocalMap
中;而ThreadLocalMap
中又維護了一個Entry對象,也就說數據最終是存放到Entry對象中的;
staticclassThreadLocalMap{
staticclassEntryextendsWeakReference<ThreadLocal>>{
/**ThevalueassociatedwiththisThreadLocal.*/
Objectvalue;
Entry(ThreadLocal>k,Objectv){
super(k);
value=v;
}
}
ThreadLocalMap(ThreadLocal>firstKey,ObjectfirstValue){
table=newEntry[INITIAL_CAPACITY];
inti=firstKey.threadLocalHashCode&(INITIAL_CAPACITY-1);
table[i]=newEntry(firstKey,firstValue);
size=1;
setThreshold(INITIAL_CAPACITY);
}
//....................
}
Entry的構造方法是以當前線程為key,變量值Object為value進行存儲的;在上面的源碼中ThreadLocalMap
的構造方法中也涉及到了Entry;看到Entry是一個數組;初始化長度為INITIAL_CAPACITY = 16;
因為 Entry 繼承了 WeakReference
,在 Entry 的構造方法中,調用了 super(k)
方法就會將 threadLocal
實例包裝成一個 WeakReferenece
。這也是ThreadLocal
會產生內存泄露的原因;
內存泄露產生的原因
如圖所示存在一條引用鏈: Thread Ref->Thread->ThreadLocalMap->Entry->Key:Value
,經過上面的講解我們知道ThreadLocal
作為Key,但是被設置成了弱引用,弱引用在JVM垃圾回收時是優先回收的,就是說無論內存是否足夠弱引用對象都會被回收;弱引用的生命周期比較短;當發生一次GC的時候就會變成如下:
TreadLocalMap
中出現了Key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果線程遲遲不結束(也就是說這條引用鏈無意義的一直存在)就會造成value永遠無法回收造成內存泄露;如果當前線程運行結束Thread,ThreadLocalMap
,Entry之間沒有了引用鏈,在垃圾回收的時候就會被回收;但是在開發中我們都是使用線程池的方式,線程池的復用不會主動結束;所以還是會存在內存泄露問題;
解決方法也很簡單,就是在使用完之后主動調用remove()
方法釋放掉;
解決Hash沖突
記得在大學學習數據結構的時候學習了很多種解決hash沖突的方法;例如:
線性探測法(開放地址法的一種): 計算出的散列地址如果已被占用,則按順序找下一個空位。如果找到末尾還沒有找到空位置就從頭重新開始找;
二次探測法(開放地址法的一種)
鏈地址法:鏈地址是對每一個同義詞都建一個單鏈表來解決沖突,HashMap采用的是這種方法;
多重Hash法: 在key沖突的情況下多重hash,直到不沖突為止,這種方式不易產生堆積但是計算量太大;
公共溢出區法: 這種方式需要兩個表,一個存基礎數據,另一個存放沖突數據稱為溢出表;
上面的圖片都是在網上找到的一些資料,和大學時學習時的差不多我就直接拿來用了;也當自己復習了一遍;
介紹了那么多解決Hash沖突的方法,那ThreadLocalMap
使用的哪一種方法呢?我們可以看一下源碼:
privatevoidset(ThreadLocal>key,Objectvalue){
Entry[]tab=table;
intlen=tab.length;
//根據HashCode&數組長度計算出數組該存放的位置
inti=key.threadLocalHashCode&(len-1);
//遍歷Entry數組中的元素
for(Entrye=tab[i];
e!=null;
e=tab[i=nextIndex(i,len)]){
ThreadLocal>k=e.get();
//如果這個Entry對象的key正好是即將設置的key,那么就刷新Entry中的value;
if(k==key){
e.value=value;
return;
}
//entry!=null,key==null時,說明threadLcoal這key已經被GC了,這里就是上面說到
//會有內存泄露的地方,當然作者也知道這種情況的存在,所以這里做了一個判斷進行解決臟的
//entry(數組中不想存有過時的entry),但是也不能解決泄露問題,因為舊value還存在沒有消失
if(k==null){
//用當前插入的值代替掉這個key為null的“臟”entry
replaceStaleEntry(key,value,i);
return;
}
}
//新建entry并插入table中i處
tab[i]=newEntry(key,value);
intsz=++size;
if(!cleanSomeSlots(i,sz)&&sz>=threshold)
rehash();
}
從這里我們可以看出使用的是線性探測的方式來解決hash沖突!
源碼中通過nextIndex(i, len)
方法解決 hash 沖突的問題,該方法為((i + 1 < len) ? i + 1 : 0);
,也就是不斷往后線性探測,直到找到一個空的位置,當到哈希表末尾的時候還沒有找到空位置再從 0 開始找,成環形!
使用ThreadLocal時對象存在哪里?
在java中,棧內存歸屬于單個線程,每個線程都會有一個棧內存,其存儲的變量只能在其所屬線程中可見,即棧內存可以理解成線程的私有變量,而堆內存中的變量對所有線程可見,可以被所有線程訪問!
那么ThreadLocal
的實例以及它的值是不是存放在棧上呢?其實不是的,因為ThreadLocal
的實例實際上也是被其創建的類持有,(更頂端應該是被線程持有),而ThreadLocal
的值其實也是被線程實例持有,它們都是位于堆上,只是通過一些技巧將可見性修改成了線程可見。
審核編輯 :李倩
-
代碼
+關注
關注
30文章
4748瀏覽量
68355 -
spring
+關注
關注
0文章
338瀏覽量
14310 -
線程
+關注
關注
0文章
504瀏覽量
19651
原文標題:ThreadLocal 你真的用不上嗎?
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論