日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

再有人问你什么是ThreadLocal,就把这篇文章甩给他!

發(fā)布時間:2025/3/16 编程问答 19 豆豆
生活随笔 收集整理的這篇文章主要介紹了 再有人问你什么是ThreadLocal,就把这篇文章甩给他! 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

本文來自作者投稿,原作者:itlemon ,原文地址:https://blog.csdn.net/Lammonpeter/article/details/105175187

ThreadLocal是JDK1.2提供的一個工具,它為解決多線程程序的并發(fā)問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優(yōu)美的多線程程序,解決共享參數(shù)的頻繁傳遞與線程安全等問題。如果開發(fā)者掌握了ThreadLocal用法與原理,那么使用起來將得心應(yīng)手,那么請跟隨本文的節(jié)奏,撥開迷霧,探究本質(zhì)吧!

本文將帶領(lǐng)讀者深入理解ThreadLocal,為了保證閱讀質(zhì)量,我們可以先一起來簡單理解一下什么是ThreadLocal?

如果你從字面上來理解,很容易將ThreadLocal理解為『本地線程』,那么你就大錯特錯了。

首先,ThreadLocal不是線程,更不是本地線程,而是Thread的局部變量,也許把它命名為ThreadLocalVariable更容易讓人理解一些。

它是每個線程獨享的本地變量,每個線程都有自己的ThreadLocal,它們是線程隔離的。接下來,我們通過一個生活案例來開始理解ThreadLocal。

一、問題場景引入

假如語文老師有一本書,但是班上有30名學(xué)生,老師將這本書送給學(xué)生們?nèi)ラ喿x,30名學(xué)生都想閱讀這本書。

為保證每個學(xué)生都能閱讀到書籍,那么基本可以有兩種方案,一是按照某種排序(例如姓名首字母排序),讓每個學(xué)生依次閱讀。

二是讓30名學(xué)生同時爭搶,誰搶到誰就去閱讀,讀完放回原處,剩下的29名學(xué)生再次爭搶。

顯然第一種方案,基本表現(xiàn)為串行閱讀,時間成本較大,第二種方案為多個學(xué)生爭搶,容易發(fā)生安全問題(學(xué)生發(fā)生沖突或者書籍在爭搶過程中被毀壞)。

為了解決這兩個問題,那么有沒有更加好的方案呢?當(dāng)然有,老師可以將書籍復(fù)印30本,每個學(xué)生都發(fā)一本,這樣既大大提高了閱讀效率,節(jié)約了閱讀時間,還能保證每個學(xué)生都能有自己的書籍,這樣就不會發(fā)生爭搶,避免了安全問題。

其實閱讀到這里,讀者應(yīng)該有點感覺了,因為生動的例子能幫助讀者迅速理解關(guān)鍵點,在本例中,書籍作為共享變量,那么很多學(xué)生去爭搶,學(xué)生可以理解為線程,同時去爭搶(并發(fā)執(zhí)行)有很大可能會引起安全問題(線程安全問題),這往往是老師不愿意看到的后果。

我們在結(jié)合Java Demo來演示類似的案例。假如我們有一個需求,那就是在多線程環(huán)境下,去格式化時間為指定格式y(tǒng)yyy-MM-dd HH:mm:ss,假設(shè)一開始只有兩個線程需要這么做,代碼如下:

public?class?ThreadLocalUsage01?{public?static?void?main(String[]?args)?{new?Thread(new?Runnable()?{@Overridepublic?void?run()?{String?date?=?new?ThreadLocalUsage01().date(10);System.out.println(date);}}).start();new?Thread(new?Runnable()?{@Overridepublic?void?run()?{String?date?=?new?ThreadLocalUsage01().date(1000);System.out.println(date);}}).start();}private?String?date(int?seconds)?{//?參數(shù)的單位是毫秒,從1970.1.1?00:00:00?GMT計時Date?date?=?new?Date(1000?*?seconds);SimpleDateFormat?dateFormat?=?new?SimpleDateFormat("yyyy-MM-dd?HH:mm:ss");return?dateFormat.format(date);}}

在線程少的情況下是沒有問題的,我們在每個線程里調(diào)用date方法,也就是在每個線程里都執(zhí)行了創(chuàng)建SimpleDateFormat對象,每個對象在各自的線程里面執(zhí)行格式化時間

但是我們是否會思考到,假如有1000個線程需要格式化時間,那么需要調(diào)用1000次date方法,也就是需要創(chuàng)建1000個作用一樣的SimpleDateFormat對象,這樣是不是太浪費內(nèi)存了?也給GC帶來壓力?

于是我們聯(lián)想到,1000個線程來共享一個SimpleDateFormat對象,這樣SimpleDateFormat對象只需要創(chuàng)建一次即可,代碼如下:

public?class?ThreadLocalUsage02?{public?static?ExecutorService?THREAD_POOL?=?Executors.newFixedThreadPool(10);static?SimpleDateFormat?DATE_FORMAT?=?new?SimpleDateFormat("yyyy-MM-dd?HH:mm:ss");public?static?void?main(String[]?args)?throws?InterruptedException?{for?(int?i?=?0;?i?<?1000;?i++)?{int?finalI?=?i;THREAD_POOL.submit(new?Runnable()?{@Overridepublic?void?run()?{String?date?=?new?ThreadLocalUsage02().date(finalI);System.out.println(date);}});}//?關(guān)閉線程池,此種關(guān)閉方式不再接受新的任務(wù)提交,等待現(xiàn)有隊列中的任務(wù)全部執(zhí)行完畢之后關(guān)閉THREAD_POOL.shutdown();}private?String?date(int?seconds)?{//?參數(shù)的單位是毫秒,從1970.1.1?00:00:00?GMT計時Date?date?=?new?Date(1000?*?seconds);return?DATE_FORMAT.format(date);}}

上述代碼我們使用到了固定線程數(shù)的線程池來執(zhí)行時間格式化任務(wù),我們來執(zhí)行一下,看看結(jié)果:

截取了部分執(zhí)行結(jié)果,發(fā)現(xiàn)執(zhí)行結(jié)果中有很多重復(fù)的時間格式化內(nèi)容,這是為什么呢?

這是因為SimpleDateFormat是一個線程不安全的類,其實例對象在多線程環(huán)境下作為共享數(shù)據(jù),會發(fā)生線程不安全問題。

說到這里,很多讀者肯定會說,我們可以嘗試一下使用鎖機制,我們將date方法內(nèi)的格式化代碼使用synchronized關(guān)鍵字概括起來,保證同一時刻只能有一個線程來訪問SimpleDateFormat的format方法,代碼如下所示:

private?String?date(int?seconds)?{//?參數(shù)的單位是毫秒,從1970.1.1?00:00:00?GMT計時Date?date?=?new?Date(1000?*?seconds);String?format;synchronized?(ThreadLocalUsage02.class)?{format?=?DATE_FORMAT.format(date);}return?format; }

有了鎖的保證,那么這次執(zhí)行后就不會再出現(xiàn)重復(fù)的時間格式化結(jié)果了,這也就保證了線程安全。

使用鎖機制確實可以解決問題,但是多數(shù)情況下,我們不大愿意使用鎖,因為鎖的使用會帶來性能的下降(比如10個線程重復(fù)排隊執(zhí)行DATE_FORMAT.format(date)代碼),那么有沒有其他方法來解決這個問題呢?答案當(dāng)然是有,那就是本文的主角——ThreadLocal。

二、理解ThreadLocal的用法

這里還是使用固定線程數(shù)的線程池來執(zhí)行格式化時間的任務(wù)。

我們的基本思想是,使用ThreadLocal來給線程池中每個線程賦予一個SimpleDateFormat對象副本,該副本只能被當(dāng)前線程使用,是當(dāng)前線程獨享的成員變量,當(dāng)SimpleDateFormat對象不存在多線程共同訪問的時候,也就不會產(chǎn)生線程安全問題了,基本原理圖如下所示:

我們使用ThreadLocal的目的是為了避免創(chuàng)建1000個SimpleDateFormat對象,且在不使用鎖的情況下保證線程安全,那么如何實現(xiàn)只創(chuàng)建一個SimpleDateFormat對象且能被多個線程同時使用呢?改造后的案例代碼如下所示:

public?class?ThreadLocalUsage04?{public?static?ExecutorService?THREAD_POOL?=?Executors.newFixedThreadPool(10);public?static?void?main(String[]?args)?throws?InterruptedException?{for?(int?i?=?0;?i?<?1000;?i++)?{int?finalI?=?i;THREAD_POOL.submit(new?Runnable()?{@Overridepublic?void?run()?{String?date?=?new?ThreadLocalUsage04().date(finalI);System.out.println(date);}});}THREAD_POOL.shutdown();}private?String?date(int?seconds)?{//?參數(shù)的單位是毫秒,從1970.1.1?00:00:00?GMT計時Date?date?=?new?Date(1000?*?seconds);SimpleDateFormat?simpleDateFormat?=?ThreadSafeDateFormatter.dateFormatThreadLocal.get();return?simpleDateFormat.format(date);}}class?ThreadSafeDateFormatter?{public?static?ThreadLocal<SimpleDateFormat>?dateFormatThreadLocal?=?new?ThreadLocal<SimpleDateFormat>()?{@Overrideprotected?SimpleDateFormat?initialValue()?{return?new?SimpleDateFormat("yyyy-MM-dd?HH:mm:ss");}};}

上面的代碼使用到了ThreadLocal,將SimpleDateFormat對象用ThreadLocal包裝了一層,使得多個線程內(nèi)部都有一個SimpleDateFormat對象副本,每個線程使用自己的SimpleDateFormat,這樣就不會產(chǎn)生線程安全問題了。

那么以上介紹的是ThreadLocal的第一大場景的使用,也就是利用到了ThreadLocal的initialValue()方法,使得每個線程內(nèi)都具備了一個SimpleDateFormat副本。

接下來我們一起來看看ThreadLocal的第二大使用場景,在使用之前,我們先把兩個場景總結(jié)如下:

  • 場景1:每個線程需要一個獨享的對象,通常是工具類,比如典型的SimpleDateFormat和Random等。

  • 場景2:每個線程內(nèi)需要保存線程內(nèi)的全局變量,這樣線程在執(zhí)行多個方法的時候,可以在多個方法中獲取這個線程內(nèi)的全局變量,避免了過度參數(shù)傳遞的問題。

那么如何理解第二個問題呢?我們還是使用一個Demo來理解:假設(shè)有一個學(xué)生類,類成員變量包括姓名,性別,成績,我們需要定義三個方法來分別獲取學(xué)生的姓名、性別和成績,那么我們傳統(tǒng)的做法是:

public?class?ThreadLocalUsage05?{public?static?void?main(String[]?args)?{Student?student?=?init();new?NameService().getName(student);new?SexService().getSex(student);new?ScoreService().getScore(student);}private?static?Student?init()?{Student?student?=?new?Student();student.name?=?"Lemon";student.sex?=?"female";student.score?=?"100";return?student;}}class?Student?{/***?姓名、性別、成績*/String?name;String?sex;String?score;}class?NameService?{public?void?getName(Student?student)?{System.out.println(student.name);}}class?SexService?{public?void?getSex(Student?student)?{System.out.println(student.sex);}}class?ScoreService?{public?void?getScore(Student?student)?{System.out.println(student.score);}}

從上面的代碼中可以看出,每個類的方法都需要傳遞學(xué)生的信息才可以獲取到正確的信息,這樣做能達到目的

但是每個方法都需要學(xué)生信息作為入?yún)?#xff0c;這樣未免有點繁瑣,且在實際使用中通常在每個方法里面還需要對每個學(xué)生信息進行判空,這樣的代碼顯得十分冗余,不利于維護。

也許有人會說,我們可以將學(xué)生信息存入到一個共享的Map中,需要學(xué)生信息的時候直接去Map中取,如下圖所示:

其實這也是一種思路,但是在并發(fā)環(huán)境下,如果要使用Map,那么就需要使用同步的Map,比如ConcurrentHashMap或者Collections.SynchronizedMap(),前者底層用的是CAS和鎖機制,后者直接使用的是synchronized,性能也不盡人意。

其實,我們可以將學(xué)生信息存入到ThreadLocal中,在同一個線程中,那么直接從ThreadLocal中獲取需要的信息即可!案例代碼如下所示:

public?class?ThreadLocalUsage05?{public?static?void?main(String[]?args)?{init();new?NameService().getName();new?SexService().getSex();new?ScoreService().getScore();}private?static?void?init()?{Student?student?=?new?Student();student.name?=?"Lemon";student.sex?=?"female";student.score?=?"100";ThreadLocalProcessor.studentThreadLocal.set(student);}}class?ThreadLocalProcessor?{public?static?ThreadLocal<Student>?studentThreadLocal?=?new?ThreadLocal<>();}class?Student?{/***?姓名、性別、成績*/String?name;String?sex;String?score;}class?NameService?{public?void?getName()?{System.out.println(ThreadLocalProcessor.studentThreadLocal.get().name);}}class?SexService?{public?void?getSex()?{System.out.println(ThreadLocalProcessor.studentThreadLocal.get().sex);}}class?ScoreService?{public?void?getScore()?{System.out.println(ThreadLocalProcessor.studentThreadLocal.get().score);}}

上面的代碼就省去了頻繁的傳遞參數(shù),也沒有使用到鎖機制,同樣滿足了需求,思想其實和上面將學(xué)生信息存儲到Map中的思想差不多,只不過這里不是將學(xué)生信息存儲到Map中,而是存儲到了ThreadLocal中,原理圖如下所示:

那么總結(jié)這兩種用法,通常分別用在不同的場景里:

  • 場景一:通常多線程之間需要擁有同一個對象的副本,那么通常就采用initialValue()方法進行初始化,直接將需要擁有的對象存儲到ThreadLocal中。

  • 場景二:如果多個線程中存儲不同的信息,為了方便在其他方法里面獲取到信息,那么這種場景適合使用set()方法。例如,在攔截器生成的用戶信息,用ThreadLocal.set直接放入到ThreadLocal中去,以便在后續(xù)的方法中取出來使用。

三、理解ThreadLocal原理

3.1 理解ThreadLocalMap數(shù)據(jù)結(jié)構(gòu)

通過本文的第二小節(jié)的介紹,相信大家基本上可以掌握ThreadLocal的基本使用方法,接下來,我們來一起閱讀ThreadLocal源碼,從源碼角度來真正理解ThreadLocal。

在閱讀源碼之前,我們一起來看看一張圖片:

上圖中基本描述出了Thread、ThreadLocalMap以及ThreadLocal三者之間的包含關(guān)系。Thread類對象中維護了ThreadLocalMap成員變量,而ThreadLocalMap維護了以ThreadLocal為key,需要存儲的數(shù)據(jù)為value的Entry數(shù)組。這是它們?nèi)咧g的基本包含關(guān)系,我們需要進一步到源碼中尋找蹤跡。

查看Thread類,內(nèi)部維護了兩個變量,threadLocals和inheritableThreadLocals,它們的默認值是null,它們的類型是ThreadLocal.ThreadLocalMap,也就是ThreadLocal類的一個靜態(tài)內(nèi)部類ThreadLocalMap。

在靜態(tài)內(nèi)部類ThreadLocalMap維護一個數(shù)據(jù)結(jié)構(gòu)類型為Entry的數(shù)組,節(jié)點類型如下代碼所示:

static?class?Entry?extends?WeakReference<ThreadLocal<?>>?{/**?The?value?associated?with?this?ThreadLocal.?*/Object?value;Entry(ThreadLocal<?>?k,?Object?v)?{super(k);value?=?v;} }

從源碼中我們可以看到,Entry結(jié)構(gòu)實際上是繼承了一個ThreadLocal類型的弱引用并將其作為key,value為Object類型。這里使用弱引用是否會產(chǎn)生問題,我們這里暫時不討論,在文章結(jié)束的時候一起討論一下,暫且可以理解key就是ThreadLocal對象。對于ThreadLocalMap,我們一起來了解一下其內(nèi)部的變量:

//?默認的數(shù)組初始化容量 private?static?final?int?INITIAL_CAPACITY?=?16; //?Entry數(shù)組,大小必須為2的冪 private?Entry[]?table; //?數(shù)組內(nèi)部元素個數(shù) private?int?size?=?0; //?數(shù)組擴容閾值,默認為0,創(chuàng)建了ThreadLocalMap對象后會被重新設(shè)置 private?int?threshold;

這幾個變量和HashMap中的變量十分類似,功能也類似。

ThreadLocalMap的構(gòu)造方法如下所示:

/***?Construct?a?new?map?initially?containing?(firstKey,?firstValue).*?ThreadLocalMaps?are?constructed?lazily,?so?we?only?create*?one?when?we?have?at?least?one?entry?to?put?in?it.*/ ThreadLocalMap(ThreadLocal<?>?firstKey,?Object?firstValue)?{//?初始化Entry數(shù)組,大小?16table?=?new?Entry[INITIAL_CAPACITY];//?用第一個鍵的哈希值對初始大小取模得到索引,和HashMap的位運算代替取模原理一樣int?i?=?firstKey.threadLocalHashCode?&?(INITIAL_CAPACITY?-?1);//?將Entry對象存入數(shù)組指定位置table[i]?=?new?Entry(firstKey,?firstValue);size?=?1;//?初始化擴容閾值,第一次設(shè)置為10setThreshold(INITIAL_CAPACITY); }

從構(gòu)造方法的注釋中可以了解到,該構(gòu)造方法是懶加載的,只有當(dāng)我們創(chuàng)建一個Entry對象并需要放入到Entry數(shù)組的時候才會去初始化Entry數(shù)組。

分析到這里,也許我們都有一個疑問,平常使用ThreadLocal功能都是借助ThreadLocal對象來操作的,比如set、get、remove等,使用上都屏蔽了ThreadLocalMap的API,那么到底是如何做到的呢?我們一起繼續(xù)看下面的代碼。

3.2 理解ThreadLocal類set方法

試想我們一個請求對應(yīng)一個線程,我們可能需要在請求到達攔截器之后,可能需要校驗當(dāng)前請求的用戶信息,那么校驗通過的用戶信息通常都放入到ThreadLocalMap中,以方便在后續(xù)的方法中直接從ThreadLocalMap中獲取

但是我們并沒有直接操作ThreadLocalMap來存取數(shù)據(jù),而是通過一個靜態(tài)的ThreadLocal變量來操作,我們從上面的圖可以看出,ThreadLocalMap中存儲的鍵其實就是ThreadLocal的弱引用所關(guān)聯(lián)的對象,那么鍵是如何操作類似HashMap的值的呢?我們一起來分析一下set方法:

public?void?set(T?value)?{//?首先獲取調(diào)用此方法的線程Thread?t?=?Thread.currentThread();//?將線程傳遞到getMap方法中來獲取ThreadLocalMap,其實就是獲取到當(dāng)前線程的成員變量threadLocals所指向的ThreadLocalMap對象ThreadLocalMap?map?=?getMap(t);//?判斷Map是否為空if?(map?!=?null)//?如果Map為不空,說明當(dāng)前線程內(nèi)部已經(jīng)有ThreadLocalMap對象了,那么直接將本ThreadLocal對象作為鍵,存入的value作為值存儲到ThreadLocalMap中map.set(this,?value);else//?創(chuàng)建一個ThreadLocalMap對象并將值存入到該對象中,并賦值給當(dāng)前線程的threadLocals成員變量createMap(t,?value); }//?獲取到當(dāng)前線程的成員變量threadLocals所指向的ThreadLocalMap對象 ThreadLocalMap?getMap(Thread?t)?{return?t.threadLocals; }//?創(chuàng)建一個ThreadLocalMap對象并將值存入到該對象中,并賦值給當(dāng)前線程的threadLocals成員變量 void?createMap(Thread?t,?T?firstValue)?{t.threadLocals?=?new?ThreadLocalMap(this,?firstValue); }

上面的set方法是ThreadLocal的set方法,就是為了將指定的值存入到指定線程的threadLocals成員變量所指向的ThreadLocalMap對象中,那么具體是如何存取的,其實調(diào)用的還是ThreadLocalMap的set方法,源碼分析如下所示:

private?void?set(ThreadLocal<?>?key,?Object?value)?{//?We?don't?use?a?fast?path?as?with?get()?because?it?is?at//?least?as?common?to?use?set()?to?create?new?entries?as//?it?is?to?replace?existing?ones,?in?which?case,?a?fast//?path?would?fail?more?often?than?not.Entry[]?tab?=?table;int?len?=?tab.length;//?計算當(dāng)前ThreadLocal對象作為鍵在Entry數(shù)組中的下標(biāo)索引int?i?=?key.threadLocalHashCode?&?(len-1);//?線性遍歷,首先獲取到指定下標(biāo)的Entry對象,如果不為空,則進入到for循環(huán)體內(nèi),//?判斷當(dāng)前的ThreadLocal對象是否是同一個對象,如果是,那么直接進行值替換,并結(jié)束方法,//?如果不是,再判斷當(dāng)前Entry的key是否失效,如果失效,則直接將失效的key和值進行替換。//?這兩點都不滿足的話,那么就調(diào)用nextIndex方法進行搜尋下一個合適的位置,進行同樣的操作,//?直到找到某個位置,內(nèi)部數(shù)據(jù)為空,也就是Entry為null,那么就直接將鍵值對設(shè)置到這個位置上。//?最后判斷是否達到了擴容的條件,如果達到了,那么就進行擴容。for?(Entry?e?=?tab[i];?e?!=?null;?e?=?tab[i?=?nextIndex(i,?len)])?{ThreadLocal<?>?k?=?e.get();if?(k?==?key)?{e.value?=?value;return;}if?(k?==?null)?{replaceStaleEntry(key,?value,?i);return;}}tab[i]?=?new?Entry(key,?value);int?sz?=?++size;if?(!cleanSomeSlots(i,?sz)?&&?sz?>=?threshold)rehash(); }

這里的代碼核心的地方就是for循環(huán)這一塊,代碼上面加了詳細的注釋,這里在復(fù)述一遍:

線性遍歷,首先獲取到指定下標(biāo)的Entry對象,如果不為空,則進入到for循環(huán)體內(nèi),判斷當(dāng)前的ThreadLocal對象是否是同一個對象

如果是,那么直接進行值替換,并結(jié)束方法。如果不是,再判斷當(dāng)前Entry的key是否失效,如果失效,則直接將失效的key和值進行替換。

這兩點都不滿足的話,那么就調(diào)用nextIndex方法進行搜尋下一個合適的位置,進行同樣的操作,直到找到某個位置,內(nèi)部數(shù)據(jù)為空,也就是Entry為null,那么就直接將鍵值對設(shè)置到這個位置上。最后判斷是否達到了擴容的條件,如果達到了,那么就進行擴容。

這里有兩點需要注意:一是nextIndex方法,二是key失效,這里先解釋第一個注意點,第二個注意點涉及到弱引用JVM GC問題,文章最后做出解釋。

nextIndex方法的具體代碼如下所示:

private?static?int?nextIndex(int?i,?int?len)?{return?((i?+?1?<?len)???i?+?1?:?0); }

其實就是尋找下一個合適位置,找到最后一個后還不合適的話,那么從數(shù)組頭部重新開始找,且一定可以找到,因為存在擴容閾值,數(shù)組必定有冗余的位置存放當(dāng)前鍵值對所對應(yīng)的Entry對象。其實nextIndex方法就是大名鼎鼎的『開放尋址法』的應(yīng)用。

這一點和HashMap不一樣,HashMap存儲HashEntry對象發(fā)生哈希沖突的時候采用的是鏈表方式進行存儲,而這里是去尋找下一個合適的位置,思想就是『開放尋址法』。

3.3 理解ThreadLocal類get方法

在實際的開發(fā)中,我們往往需要在代碼中調(diào)用ThreadLocal對象的get方法來獲取存儲在ThreadLocalMap中的數(shù)據(jù),具體的源碼如下所示:

public?T?get()?{//?獲取當(dāng)前線程的ThreadLocalMap對象Thread?t?=?Thread.currentThread();ThreadLocalMap?map?=?getMap(t);if?(map?!=?null)?{//?如果map不為空,那么嘗試獲取Entry數(shù)組中以當(dāng)前ThreadLocal對象為鍵的Entry對象ThreadLocalMap.Entry?e?=?map.getEntry(this);if?(e?!=?null)?{//?如果找到,那么直接返回value@SuppressWarnings("unchecked")T?result?=?(T)e.value;return?result;}}//?如果Map為空或者在Entry數(shù)組中沒有找到以當(dāng)前ThreadLocal對象為鍵的Entry對象,//?那么就在這里進行值初始化,值初始化的過程是將null作為值,當(dāng)前ThreadLocal對象作為鍵,//?存入到當(dāng)前線程的ThreadLocalMap對象中return?setInitialValue(); }//?值初始化過程 private?T?setInitialValue()?{T?value?=?initialValue();Thread?t?=?Thread.currentThread();ThreadLocalMap?map?=?getMap(t);if?(map?!=?null)map.set(this,?value);elsecreateMap(t,?value);return?value; }

值初始化過程是這樣的一個過程,如果調(diào)用新的ThreadLocal對象的get方法,那么在當(dāng)前線程的成員變量threadLocals中必定不存在key為當(dāng)前ThreadLocal對象的Entry對象,那么這里值初始話就將此ThreadLocal對象作為key,null作為值存儲到ThreadLocalMap的Entry數(shù)組中。

3.4 理解ThreadLocal的remove方法

使用ThreadLocal這個工具的時候,一般提倡使用完后及時清理存儲在ThreadLocalMap中的值,防止內(nèi)存泄露。這里一起來看下ThreadLocal的remove方法。

public?void?remove()?{ThreadLocalMap?m?=?getMap(Thread.currentThread());if?(m?!=?null)m.remove(this); }//?具體的刪除指定的值,也是通過遍歷尋找,找到就刪除,找不到就算了 private?void?remove(ThreadLocal<?>?key)?{Entry[]?tab?=?table;int?len?=?tab.length;int?i?=?key.threadLocalHashCode?&?(len-1);for?(Entry?e?=?tab[i];?e?!=?null;?e?=?tab[i?=?nextIndex(i,?len)])?{if?(e.get()?==?key)?{e.clear();expungeStaleEntry(i);return;}} }

看了這么多ThreadLocal的源碼實現(xiàn),其實原理還是很簡單的,基本上可以說是一看就懂,理解ThreadLocal原理,其實就是需要理清Thread、ThreadLocal、ThreadLocalMap三者之間的關(guān)系

這里加以總結(jié):線程類Thread內(nèi)部持有ThreadLocalMap的成員變量,而ThreadLocalMap是ThreadLocal的內(nèi)部類,ThreadLocal操作了ThreadLocalMap對象內(nèi)部的數(shù)據(jù),對外暴露的都是ThreadLocal的方法API,隱藏了ThreadLocalMap的具體實現(xiàn),理清了這一點,ThreadLocal就很容易理解了。

四、理解ThreadLocalMap內(nèi)存泄露問題

這里所說的ThreadLocal的內(nèi)存泄露問題,其實都是從ThreadLocalMap中的一段代碼說起的,這段代碼就是Entry的構(gòu)造方法:

static?class?Entry?extends?WeakReference<ThreadLocal<?>>?{/**?The?value?associated?with?this?ThreadLocal.?*/Object?value;Entry(ThreadLocal<?>?k,?Object?v)?{super(k);value?=?v;} }

這里簡單介紹一下Java內(nèi)的四大引用:

  • 強引用:Java中默認的引用類型,一個對象如果具有強引用那么只要這種引用還存在就不會被回收。比如String str = new String("Hello ThreadLocal");,其中str就是一個強引用,當(dāng)然,一旦強引用出了其作用域,那么強引用隨著方法彈出線程棧,那么它所指向的對象將在合適的時機被JVM垃圾收集器回收。

  • 軟引用:如果一個對象具有軟引用,在JVM發(fā)生內(nèi)存溢出之前(即內(nèi)存充足夠使用),是不會GC這個對象的;只有到JVM內(nèi)存不足的時候才會調(diào)用垃圾回收期回收掉這個對象。軟引用和一個引用隊列聯(lián)合使用,如果軟引用所引用的對象被回收之后,該引用就會加入到與之關(guān)聯(lián)的引用隊列中。

  • 弱引用:這里討論ThreadLocalMap中的Entry類的重點,如果一個對象只具有弱引用,那么這個對象就會被垃圾回收器回收掉(被弱引用所引用的對象只能生存到下一次GC之前,當(dāng)發(fā)生GC時候,無論當(dāng)前內(nèi)存是否足夠,弱引用所引用的對象都會被回收掉)。弱引用也是和一個引用隊列聯(lián)合使用,如果弱引用的對象被垃圾回收期回收掉,JVM會將這個引用加入到與之關(guān)聯(lián)的引用隊列中。若引用的對象可以通過弱引用的get方法得到,當(dāng)引用的對象被回收掉之后,再調(diào)用get方法就會返回null。

  • 虛引用:虛引用是所有引用中最弱的一種引用,其存在就是為了將關(guān)聯(lián)虛引用的對象在被GC掉之后收到一個通知。

我們從ThreadLocal的內(nèi)部靜態(tài)類Entry的代碼設(shè)計可知,ThreadLocal的引用k通過構(gòu)造方法傳遞給了Entry類的父類WeakReference的構(gòu)造方法,從這個層面來說,可以理解ThreadLocalMap中的鍵是ThreadLocal的所引用。

當(dāng)一個線程調(diào)用ThreadLocal的set方法設(shè)置變量的時候,當(dāng)前線程的ThreadLocalMap就會存放一個記錄,這個記錄的鍵為ThreadLocal的弱引用,value就是通過set設(shè)置的值,這個value值被強引用。

如果當(dāng)前線程一直存在且沒有調(diào)用該ThreadLocal的remove方法,如果這個時候別的地方還有對ThreadLocal的引用,那么當(dāng)前線程中的ThreadLocalMap中會存在對ThreadLocal變量的引用和value對象的引用,是不會釋放的,就會造成內(nèi)存泄漏。

考慮這個ThreadLocal變量沒有其他強依賴,如果當(dāng)前線程還存在,由于線程的ThreadLocalMap里面的key是弱引用,所以當(dāng)前線程的ThreadLocalMap里面的ThreadLocal變量的弱引用在垃圾回收的時候就被回收,但是對應(yīng)的value還是存在的這就可能造成內(nèi)存泄漏(因為這個時候ThreadLocalMap會存在key為null但是value不為null的entry項)。

總結(jié):ThreadLocalMap中的Entry的key使用的是ThreadLocal對象的弱引用,在沒有其他地方對ThreadLocal依賴,ThreadLocalMap中的ThreadLocal對象就會被回收掉,但是對應(yīng)的值不會被回收,這個時候Map中就可能存在key為null但是值不為null的項,所以在使用ThreadLocal的時候要養(yǎng)成及時remove的習(xí)慣。

往期推薦

2020中國大學(xué)排名出爐,第一不出預(yù)料,第三也情有可原,第五竟然是他?

遇到網(wǎng)絡(luò)問題你是怎么解決的?

全網(wǎng)17萬瀏覽量的PDF免費下載!《〈Java開發(fā)手冊〉靈魂13問》

本文由“壹伴編輯器”提供技術(shù)支

?

直面Java第318期:什么是STOP THE WORLD機制?

深入并發(fā)第013期:拓展synchronized——鎖優(yōu)化

如果你喜歡本文,

請長按二維碼,關(guān)注?Hollis.

轉(zhuǎn)發(fā)至朋友圈,是對我最大的支持。

點個?在看?

喜歡是一種感覺

在看是一種支持

↘↘↘

總結(jié)

以上是生活随笔為你收集整理的再有人问你什么是ThreadLocal,就把这篇文章甩给他!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。