精品国产人成在线_亚洲高清无码在线观看_国产在线视频国产永久2021_国产AV综合第一页一个的一区免费影院黑人_最近中文字幕MV高清在线视频

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

ThreadLocal的作用以及應用場景

倩倩 ? 來源:juejin.cn ? 作者:芋道源碼 ? 2022-09-19 10:56 ? 次閱讀


ThreadLocal的作用以及應用場景

ThreadLocal算是一種并發容器吧,因為他的內部是有ThreadLocalMap組成,ThreadLocal是為了解決多線程情況下變量不能被共享的問題,也就是多線程共享變量的問題。

ThreadLocalLock以及Synchronized的區別是:ThreadLocal是給每個線程分配一個變量(對象),各個線程都存有變量的副本,這樣每個線程都是使用自己(變量)對象實例,使線程與線程之間進行隔離;而LockSynchronized的方式是使線程有順序的執行。

舉一個簡單的例子:目前有100個學生等待簽字,但是老師只有一個筆,那老師只能按順序的分給每個學生,等待A學生簽字完成然后將筆交給B學生,這就類似LockSynchronized的方式。而ThreadLocal是,老師直接拿出一百個筆給每個學生;再效率提高的同事也要付出一個內存消耗;也就是以空間換時間的概念

基于 Spring Boot + MyBatis Plus + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能

  • 項目地址:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • 視頻教程:https://doc.iocoder.cn/video/

使用場景

Spring的事務隔離就是使用ThreadLocal和AOP來解決的;主要是TransactionSynchronizationManager這個類;

解決SimpleDateFormat線程不安全問題;

當我們使用SimpleDateFormatparse()方法的時候,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 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能

  • 項目地址:https://gitee.com/zhijiantianya/yudao-cloud
  • 視頻教程:https://doc.iocoder.cn/video/

原理分析

上面我們基本上知道了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(ThreadLocalfirstKey,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(ThreadLocalk,Objectv){
super(k);
value=v;
}

}
ThreadLocalMap(ThreadLocalfirstKey,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會產生內存泄露的原因;

內存泄露產生的原因

a6f698ac-37c4-11ed-ba43-dac502259ad0.png

如圖所示存在一條引用鏈: Thread Ref->Thread->ThreadLocalMap->Entry->Key:Value,經過上面的講解我們知道ThreadLocal作為Key,但是被設置成了弱引用,弱引用在JVM垃圾回收時是優先回收的,就是說無論內存是否足夠弱引用對象都會被回收;弱引用的生命周期比較短;當發生一次GC的時候就會變成如下:

a724c54c-37c4-11ed-ba43-dac502259ad0.png

TreadLocalMap中出現了Key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果線程遲遲不結束(也就是說這條引用鏈無意義的一直存在)就會造成value永遠無法回收造成內存泄露;如果當前線程運行結束Thread,ThreadLocalMap,Entry之間沒有了引用鏈,在垃圾回收的時候就會被回收;但是在開發中我們都是使用線程池的方式,線程池的復用不會主動結束;所以還是會存在內存泄露問題;

解決方法也很簡單,就是在使用完之后主動調用remove()方法釋放掉;

解決Hash沖突

記得在大學學習數據結構的時候學習了很多種解決hash沖突的方法;例如:

線性探測法(開放地址法的一種): 計算出的散列地址如果已被占用,則按順序找下一個空位。如果找到末尾還沒有找到空位置就從頭重新開始找;

a7660520-37c4-11ed-ba43-dac502259ad0.png

二次探測法(開放地址法的一種)

a7c2781e-37c4-11ed-ba43-dac502259ad0.png

鏈地址法:鏈地址是對每一個同義詞都建一個單鏈表來解決沖突,HashMap采用的是這種方法;

aa7fe488-37c4-11ed-ba43-dac502259ad0.png

多重Hash法: 在key沖突的情況下多重hash,直到不沖突為止,這種方式不易產生堆積但是計算量太大;

公共溢出區法: 這種方式需要兩個表,一個存基礎數據,另一個存放沖突數據稱為溢出表;

上面的圖片都是在網上找到的一些資料,和大學時學習時的差不多我就直接拿來用了;也當自己復習了一遍;

介紹了那么多解決Hash沖突的方法,那ThreadLocalMap使用的哪一種方法呢?我們可以看一下源碼:

privatevoidset(ThreadLocalkey,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)]){
ThreadLocalk=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 你真的用不上嗎?

文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    系統放大器的技術原理和應用場景

    系統放大器是一種重要的電子設備,其技術原理和應用場景都具有一定的專業性和廣泛性。以下是對系統放大器的技術原理和應用場景的詳細介紹:一、技術原理系統放大器的工作原理基于電子器件的非線性特性,通過控制
    發表于 11-18 14:46

    寬帶放大器的技術原理和應用場景

    的均衡性和穩定性。這要求寬帶放大器在設計時,需要考慮多種因素,如帶寬、增益、輸出功率以及在各種幅度和相位條件下失配的行為特性、線性度、諧波特性、功率效率等,并進行綜合考慮和優化。二、應用場景寬帶放大器因其
    發表于 11-13 14:35

    移動終端測試儀的技術原理和應用場景

    ,確保設備在導航服務中的準確性和可靠性。 應用場景移動終端測試儀的應用場景廣泛,涵蓋了從研發到生產、從維護到監管的多個環節: 移動維修服務:維修技術人員可以使用便攜的綜測儀快速對手機進行全面檢測
    發表于 11-04 16:01

    實時示波器的技術原理和應用場景

    有頻譜分析功能,可以將時域信號轉換為頻域信號,從而顯示信號的頻譜特性。綜上所述,實時示波器憑借其獨特的技術原理和廣泛的應用場景,在電子工程和通信技術領域發揮著不可替代的作用
    發表于 10-23 14:22

    源測量單元設備的技術原理和應用場景

    ,SMU的功能和應用領域也在不斷擴展,其在電子測試中的重要性不言而喻。綜上所述,SMU設備憑借其獨特的技術原理和廣泛的應用場景,在現代電子測試中發揮著不可或缺的作用。對于工程師來說,深入了解和靈活應用SMU將是提升電路板測試效果的關鍵所在。
    發表于 10-22 11:10

    參數分析儀的技術原理和應用場景

    參數分析儀的技術原理和應用場景因其具體類型和用途的不同而有所差異。以下是對參數分析儀技術原理和應用場景的詳細歸納: 技術原理 基于物理性質的測量: 某些參數分析儀通過測量樣品的物理性質(如電阻
    發表于 10-17 14:42

    智能IC卡測試設備的技術原理和應用場景

    用場景中的可靠性和安全性。 綜上所述,智能IC卡測試設備在保障IC卡質量和性能、提高系統安全性和穩定性等方面發揮著重要作用。隨著技術的不斷發展和應用場景的不斷拓展,智能IC卡測試設備的應用前景將更加廣闊。
    發表于 09-26 14:27

    脈沖式線圈測試儀的技術原理和應用場景

    測試儀憑借其高效、非破壞性的測試原理以及廣泛的應用場景,在電氣制造和檢測領域發揮著重要作用。通過使用該測試儀,企業可以及時發現并解決線圈質量問題,提高產品的可靠性和安全性。
    發表于 09-18 14:29

    STM32待機模式適合用于那些應用場景

    ,再不濟寫入flash也行!! 我能想到的就是用rtc喚醒,達到定時開機作用了 因此實在是想不到待機模式的其他應用場景,求大神給予提示!!還有,待機模式喚醒后,有程序執行入口么?
    發表于 05-07 07:46

    NanoEdge AI的技術原理、應用場景及優勢

    能耗并提高數據安全性。本文將對 NanoEdge AI 的技術原理、應用場景以及優勢進行綜述。 1、技術原理 NanoEdge AI 的核心技術包括邊緣計算、神經網絡壓縮和低功耗硬件設計。邊緣計算
    發表于 03-12 08:09

    AG32VF-MIPI應用場景

    to 1.2Gbps MIPI D-PHY以及DSI硬核 AG32VF-MIPI的應用場景,包括了基本的MIPI屏幕驅動,以及各種顯示橋接場合,如下圖所示。 AG32VF-MIPI系列產品即將正式發布。
    發表于 01-22 08:56

    前置微小信號放大器的作用和應用場景有哪些

    在電子設備中,前置微小信號放大器扮演著非常重要的角色,其作用和應用場景有許多。在本文中,Aigtek安泰電子將詳細討論前置微小信號放大器的作用和其在通信、醫療、音頻和測量等領域的應用場景
    的頭像 發表于 12-18 16:46 ?570次閱讀
    前置微小信號放大器的<b class='flag-5'>作用</b>和應<b class='flag-5'>用場景</b>有哪些

    電纜屏蔽層的作用 電纜屏蔽層的種類和使用場景

    的穩定傳輸,同時也能夠防止電纜內部電磁輻射對周圍環境產生干擾。本文將詳細介紹電纜屏蔽層的作用、種類和使用場景以及電纜屏蔽層正確的接地做法和注意事項。 首先,電纜屏蔽層的作用是保護電纜
    的頭像 發表于 12-11 15:05 ?2041次閱讀

    壓敏電阻的工作原理、分類、特性以及用場景

    壓敏電阻的工作原理、分類、特性以及用場景 壓敏電阻是一種利用壓力變化來改變電阻值的傳感器。其作用是將外部的機械壓力、力量或負荷轉化為電氣信號,從而實現對物理量的測量。壓敏電阻常用于力的測量、壓力
    的頭像 發表于 12-08 15:47 ?2381次閱讀

    Golang接口的作用和應用場景

    代碼的靈活性、可擴展性和可維護性。本文將深入探討Golang接口的作用、應用場景,并通過實際案例展示其在實際開發中的應用。
    的頭像 發表于 12-05 10:44 ?1094次閱讀