<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          ThreadLocal全攻略:使用實(shí)戰(zhàn),源碼分析,內(nèi)存泄露分析

          共 8561字,需瀏覽 18分鐘

           ·

          2021-04-28 21:40

          前言

          說起ThreadLocal即便你沒有直接用到過,它也間接的出現(xiàn)在你使用過的框架里,比如Spring的事物管理,Hibernate的Session管理、logback(和log4j)中的MDC功能實(shí)現(xiàn)等。而在項(xiàng)目開發(fā)中,比如用到的一些分頁(yè)功能的實(shí)現(xiàn)往往也會(huì)借助于ThreadLocal。

          正是因?yàn)門hreadLocal的無處不在,所以在面試的時(shí)候也經(jīng)常會(huì)被問到它的實(shí)現(xiàn)原理、核心API使用以及內(nèi)存泄露的問題。

          而且基于這些問題還可以拓展到線程安全方面、JVM內(nèi)存管理與分析、Hash算法等等知識(shí)點(diǎn)。可見ThreadLocal對(duì)開發(fā)人員來說是多么的重要的。如果你還沒有全面的了解,那么這篇文章值得你深入學(xué)習(xí)一下。

          什么是ThreadLocal

          ThreadLocal是Therad的局部變量的維護(hù)類,在Java中是作為一個(gè)特殊的變量存儲(chǔ)在。當(dāng)使用ThreadLocal維護(hù)變量時(shí),ThreadLocal為每個(gè)使用該變量的線程提供獨(dú)立的變量副本,所以每一個(gè)線程都可以獨(dú)立地改變自己的副本,而不會(huì)影響其它線程所對(duì)應(yīng)的副本。

          因?yàn)槊總€(gè)Thread內(nèi)有自己的實(shí)例副本,且該副本只能由當(dāng)前Thread使用,也就不存在多線程間共享的問題。

          總的來說,ThreadLocal適用于每個(gè)線程需要自己獨(dú)立的實(shí)例且該實(shí)例需要在多個(gè)方法中被使用,也即變量在線程間隔離而在方法或類間共享的場(chǎng)景。

          比如,有一個(gè)變量count,在多線程并發(fā)時(shí)操作count++會(huì)出現(xiàn)線程安全問題。但是通過ThreadLocal就可以為每個(gè)線程創(chuàng)建只屬于當(dāng)前線程的count副本,各自操作各自的副本,不會(huì)影響到其他線程。

          從另外一個(gè)角度來說,ThreadLocal是一個(gè)數(shù)據(jù)結(jié)構(gòu),有點(diǎn)像HashMap,可以保存"key:value"鍵值對(duì),但是一個(gè)ThreadLocal只能保存一個(gè)鍵值對(duì),各個(gè)線程的數(shù)據(jù)互不干擾。

          @Test
          public void test1(){
          ThreadLocal<String> localName = new ThreadLocal<>();
          // 只提供了一個(gè)set方法;
          localName.set("程序新視界");
          // 同時(shí)只提供了一個(gè)get方法
          String name = localName.get();
          System.out.println(name);
          }

          上述代碼中線程A初始化了一個(gè)ThreadLocal對(duì)象,并調(diào)用set方法,保持了一個(gè)值。而這個(gè)值只能線程A調(diào)用get方法才能獲取到。如果此時(shí)線程B調(diào)用get方法是無法獲取到的。至于如何實(shí)現(xiàn)這一功能的,我們?cè)诤竺嬖创a分析中進(jìn)行講解,這里知道其功能即可。

          ThreadLocal使用實(shí)例

          上面介紹了使用場(chǎng)景和基本的實(shí)現(xiàn)理論,下面我們就來通過一個(gè)簡(jiǎn)單的實(shí)例看一下如何使用ThreadLocal。

          public class ThreadLocalMain {

          /**
          * ThreadLocal變量,每個(gè)線程都有一個(gè)副本,互不干擾
          */

          public static final ThreadLocal<String> HOLDER = new ThreadLocal<>();

          public static void main(String[] args) throws Exception {
          new ThreadLocalMain().execute();
          }

          public void execute() throws Exception {
          // 主線程設(shè)置值
          HOLDER.set("程序新視界");
          System.out.println(Thread.currentThread().getName() + "線程ThreadLocal中的值:" + HOLDER.get());

          new Thread(() -> {
          System.out.println(Thread.currentThread().getName() + "線程ThreadLocal中的值:" + HOLDER.get());
          // 設(shè)置當(dāng)前線程中的值
          HOLDER.set("《程序新視界》");
          System.out.println("重新設(shè)置之后," + Thread.currentThread().getName() + "線程ThreadLocal中的值:" + HOLDER.get());
          System.out.println(Thread.currentThread().getName() + "線程執(zhí)行結(jié)束");
          }).start();
          // 等待所有線程執(zhí)行結(jié)束
          Thread.sleep(1000L);
          System.out.println(Thread.currentThread().getName() + "線程ThreadLocal中的值:" + HOLDER.get());
          }

          }

          示例中定義了一個(gè)static final的ThreadLocal變量HOLDER,在main方法中模擬通過兩個(gè)線程來操作HOLDER中存儲(chǔ)的值。先對(duì)HOLDER設(shè)置一個(gè)值,然后打印獲取得到的值,然后新起一個(gè)線程去修改HOLDER中的值,然后分別在新線程和主線程兩處獲取對(duì)應(yīng)的值。

          執(zhí)行程序,打印結(jié)果如下:

          main線程ThreadLocal中的值:程序新視界
          Thread-0線程ThreadLocal中的值:null
          重新設(shè)置之后,Thread-0線程ThreadLocal中的值:《程序新視界》
          Thread-0線程執(zhí)行結(jié)束
          main線程ThreadLocal中的值:程序新視界

          對(duì)照程序和輸出結(jié)果,你會(huì)發(fā)現(xiàn),主線程和Thread-0各自獨(dú)享自己的變量存儲(chǔ)。主線程并沒有因?yàn)門hread-0調(diào)用了HOLDER的set方法而被改變。

          之所以能達(dá)到這個(gè)效果,正是因?yàn)樵赥hreadLocal中,每個(gè)線程Thread擁有一份自己的副本變量,多個(gè)線程互不干擾。那么,你會(huì)疑惑,ThreadLocal是如何實(shí)現(xiàn)這一功能的呢?

          ThreadLocal原理分析

          在學(xué)習(xí)ThreadLocal的原理之前,我們先來看一些相關(guān)的理論知識(shí)和數(shù)據(jù)結(jié)構(gòu)。

          基本流程與源碼實(shí)現(xiàn)

          一個(gè)線程內(nèi)可以存多個(gè)ThreadLocal對(duì)象,存儲(chǔ)的位置位于Thread的ThreadLocal.ThreadLocalMap變量,在Thread中有如下變量:

          /* ThreadLocal values pertaining to this thread. This map is maintained
          * by the ThreadLocal class. */
          ThreadLocal.ThreadLocalMap threadLocals = null;

          ThreadLocalMap是由ThreadLocal維護(hù)的靜態(tài)內(nèi)部類,正如代碼中注解所說這個(gè)變量是由ThreadLocal維護(hù)的。

          我們?cè)谑褂肨hreadLocal的get()、set()方法時(shí),其實(shí)都是調(diào)用了ThreadLocalMap類對(duì)應(yīng)的get()、set()方法。

          Thread中的這個(gè)變量的初始化通常是在首次調(diào)用ThreadLocal的get()、set()方法時(shí)進(jìn)行的。

          public void set(T value) {
          Thread t = Thread.currentThread();
          ThreadLocalMap map = getMap(t);
          if (map != null)
          map.set(this, value);
          else
          createMap(t, value);
          }

          上述set方法中,首先獲取當(dāng)前線程對(duì)象,然后通過getMap方法來獲取當(dāng)前線程中的threadLocals:

          ThreadLocalMap getMap(Thread t) {
          return t.threadLocals;
          }

          如果Thread中的對(duì)應(yīng)屬性為null,則創(chuàng)建一個(gè)ThreadLocalMap并賦值給Thread:

          void createMap(Thread t, T firstValue) {
          t.threadLocals = new ThreadLocalMap(this, firstValue);
          }

          如果已經(jīng)存在,則通過ThreadLocalMap的set方法設(shè)置值,這里我們可以看到set中key為this,也就是當(dāng)前ThreadLocal對(duì)象,而value值則是我們要存的值。

          對(duì)應(yīng)的get方法源碼如下:

          public T get() {
          Thread t = Thread.currentThread();
          ThreadLocalMap map = getMap(t);
          if (map != null) {
          ThreadLocalMap.Entry e = map.getEntry(this);
          if (e != null) {
          @SuppressWarnings("unchecked")
          T result = (T)e.value;
          return result;
          }
          }
          return setInitialValue();
          }

          可以看到同樣通過當(dāng)前線程,拿到當(dāng)前線程的threadLocals屬性,然后從中獲取存儲(chǔ)的值并返回。在get的時(shí)候,如果Thread中的threadLocals屬性未進(jìn)行初始化,則也會(huì)間接調(diào)用createMap方法進(jìn)行初始化操作。

          下面我們通過一個(gè)流程圖來匯總一下上述流程: 

          上述流程中給Thread的threadLocals屬性初始化的操作,在JDK8和9中通過debug發(fā)現(xiàn),都沒有走createMap方法,暫時(shí)還不清楚JVM是如何進(jìn)行初始化賦值的。而在測(cè)試JDK13和JDK14的時(shí)候,很明顯走了createMap方法。

          ThreadLoalMap的數(shù)據(jù)結(jié)構(gòu)

          ThreadLoalMap是ThreadLocal中的一個(gè)靜態(tài)內(nèi)部類,類似HashMap的數(shù)據(jù)結(jié)構(gòu),但并沒有實(shí)現(xiàn)Map接口。

          ThreadLoalMap中初始化了一個(gè)大小16的Entry數(shù)組,Entry對(duì)象用來保存每一個(gè)key-value鍵值對(duì)。通過上面的set方法,我們已經(jīng)知道其中的key永遠(yuǎn)都是ThreadLocal對(duì)象。

          看一下相關(guān)的源碼:

          static class ThreadLocalMap {

          static class Entry extends WeakReference<ThreadLocal<?>> {
          /** The value associated with this ThreadLocal. */
          Object value;

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

          private static final int INITIAL_CAPACITY = 16;

          // ...
          }

          ThreadLoalMap的類圖結(jié)構(gòu)如下: 

          這里需要留意的是,ThreadLocalMap類中的Entry對(duì)象繼承自WeakReference,也就是說它是弱引用。這里會(huì)出現(xiàn)內(nèi)存泄露的情況,后續(xù)會(huì)講到。

          由于hreadLocalMaps是延遲創(chuàng)建的,因此在構(gòu)造時(shí)至少要?jiǎng)?chuàng)建一個(gè)Entry對(duì)象。這里可以從構(gòu)造方法中看到:

          ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
          table = new Entry[INITIAL_CAPACITY];
          int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
          table[i] = new Entry(firstKey, firstValue);
          size = 1;
          setThreshold(INITIAL_CAPACITY);
          }

          上述構(gòu)造方法,創(chuàng)建了一個(gè)默認(rèn)長(zhǎng)度為16的Entry數(shù)組,通過hashCode與length位運(yùn)算確定索引值i。而上面也提到,每個(gè)Thread都有一個(gè)ThreadLocalMap類型的變量。

          至此,結(jié)合Thread,我們可以看到整個(gè)數(shù)據(jù)模型如下:

          hash沖突及解決

          我們留意到構(gòu)造方法中Entry在table中存儲(chǔ)位置是通過hashcode算法獲得。每個(gè)ThreadLocal對(duì)象都有一個(gè)hash值threadLocalHashCode,每初始化一個(gè)ThreadLocal對(duì)象,hash值就增加一個(gè)固定的大小0x61c88647。

          在向ThreadLocalMap中的Entry數(shù)值存儲(chǔ)Entry對(duì)象時(shí),會(huì)根據(jù)ThreadLocal對(duì)象的hash值,定位到table中的位置i。這里分三種情況:

          • 如果當(dāng)前位置為空的,直接將Entry存放在對(duì)應(yīng)位置;

          • 如果位置i已經(jīng)有值且這個(gè)Entry對(duì)象的key正好是即將設(shè)置的key,那么重新設(shè)置Entry中的value;

          • 如果位置i的Entry對(duì)象和即將設(shè)置的key沒關(guān)系,則尋找一個(gè)空位置;

          計(jì)算hash值便會(huì)有hash沖突出現(xiàn),常見的解決方法有:再哈希法、開放地址法、建立公共溢出區(qū)、鏈?zhǔn)降刂贩ǖ取?/span>

          上面的流程可以看出這里采用的是開放地址方法,如果當(dāng)前位置有值,就繼續(xù)尋找下一個(gè)位置,注意table[len-1]的下一個(gè)位置是table[0],就像是一個(gè)環(huán)形數(shù)組,所以也叫閉散列法。如果一直都找不到空位置就會(huì)出現(xiàn)死循環(huán),發(fā)生內(nèi)存溢出。當(dāng)然有擴(kuò)容機(jī)制,一般不會(huì)找不到空位置的。

          ThreadLocal內(nèi)存泄露

          ThreadLocal使用不當(dāng)可能會(huì)出現(xiàn)內(nèi)存泄露,進(jìn)而可能導(dǎo)致內(nèi)存溢出。下面我們就來分析一下內(nèi)存泄露的原因及相關(guān)設(shè)計(jì)思想。

          內(nèi)存引用鏈路

          根據(jù)前面對(duì)ThreadLocal的分析,得知每個(gè)Thread維護(hù)一個(gè)ThreadLocalMap,它key是ThreadLocal實(shí)例本身,value是業(yè)務(wù)需要存儲(chǔ)的Object。也就是說ThreadLocal本身并不存儲(chǔ)值,它只是作為一個(gè)key來讓線程從ThreadLocalMap獲取value。

          仔細(xì)觀察ThreadLocalMap,這個(gè)map是使用ThreadLocal的弱引用作為Key的,弱引用的對(duì)象在GC時(shí)會(huì)被回收。因此使用了ThreadLocal后,引用鏈如圖所示:

          其中虛線表示弱引用。下面我們先來了解一下Java中引用的分類。

          Java中的引用

          Java中通常會(huì)存在以下類型的引用:強(qiáng)引用、弱引用、軟引用、虛引用。

          • 強(qiáng)引用:通常new出來的對(duì)象就是強(qiáng)引用類型,只要引用存在,垃圾回收器將永遠(yuǎn)不會(huì)回收被引用的對(duì)象,哪怕內(nèi)存不足的時(shí)候;

          • 軟引用:使用SoftReference修飾的對(duì)象被稱為軟引用,軟引用指向的對(duì)象在內(nèi)存要溢出的時(shí)候被回收。如果回收之后,還沒有足夠的內(nèi)存,才會(huì)拋出內(nèi)存溢出異常;

          • 弱引用:使用WeakReference修飾的對(duì)象被稱為弱引用,只要發(fā)生垃圾回收,無論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象實(shí)例。

          • 虛引用:虛引用是最弱的引用,在Java中使用PhantomReference進(jìn)行定義。虛引用中唯一的作用就是用隊(duì)列接收對(duì)象即將死亡的通知。

          泄露原因分析

          正常來說,當(dāng)Thread執(zhí)行完會(huì)被銷毀,Thread.threadLocals指向的ThreadLocalMap實(shí)例也隨之變?yōu)槔锩娲娣诺腅ntity也會(huì)被回收。這種情況是不會(huì)發(fā)生內(nèi)存泄漏的。

          發(fā)生內(nèi)存泄露的場(chǎng)景一般存在于線程池的情況下。此時(shí),Thread生命周期比較長(zhǎng)(存在循環(huán)使用),threadLocals引用一直存在,當(dāng)其存放的ThreadLocal被回收(弱引用生命周期比較短)后,對(duì)應(yīng)的Entity就成了key為null的實(shí)例,但value值不會(huì)被回收。如果此Entity一直不被get()、set()、remove(),就一直不會(huì)被回收,也就發(fā)生了內(nèi)存泄漏。

          所以,通常在使用完ThreadLocal后需要調(diào)用remove()方法進(jìn)行內(nèi)存的清除。

          比如在web請(qǐng)求當(dāng)中,我們可以通過過濾器等進(jìn)行回收方法的調(diào)用:

          public void doFilter(ServeletRequest request, ServletResponse){
          try{
          //設(shè)置ThreadLocal變量
          localName.set("程序新視界");
          chain.doFilter(request, response)
          }finally{
          //調(diào)用remove方法溢出threadLocal中的變量
          localName.remove();
          }
          }

          為什么使用弱引用而不是強(qiáng)引用?

          從表面上看內(nèi)存泄漏的根源在于使用了弱引用,但為什么JDK采用了弱引用的實(shí)現(xiàn)而不是強(qiáng)引用呢?

          先來看ThreadLocalMap類上的一段注釋:

          To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. 

          為了協(xié)助處理數(shù)據(jù)比較大并且生命周期比較長(zhǎng)的場(chǎng)景,hash table的條目使用了WeakReference作為key

          這跟我們想象的有些不同,弱引用反而是為了解決內(nèi)存存儲(chǔ)問題而專門使用的。

          我們先來假設(shè)一下,如果key使用強(qiáng)引用,那么在其他持有ThreadLocal引用的對(duì)象都回收了,但ThreadLocalMap依舊持有ThreadLocal的強(qiáng)引用,這就導(dǎo)致ThreadLocal不會(huì)被回收,從而導(dǎo)致Entry內(nèi)存泄露。

          對(duì)照一下,弱引用的情況。持有ThreadLocal引用的對(duì)象都回收了,ThreadLocalMap持有的是ThreadLocal的弱引用,會(huì)被自動(dòng)回收。只不過對(duì)應(yīng)的value值,需要在下次調(diào)用set/get/remove方法時(shí)會(huì)被清除。

          綜合對(duì)比會(huì)發(fā)現(xiàn),采用弱引用反而多了一層保障,ThreadLocal被清理后key為null,對(duì)應(yīng)的value在下一次ThreadLocalMap調(diào)用set、get、remove的時(shí)候可能會(huì)被清除。

          所以,內(nèi)存泄露的根本原因是是否手動(dòng)清除操作,而不是弱引用。

          ThreadLocal應(yīng)用場(chǎng)景

          最后,我們?cè)賮砘仡櫼幌耇hreadLocal的應(yīng)用場(chǎng)景:

          • 線程間數(shù)據(jù)隔離,各線程的ThreadLocal互不影響;

          • 方便同一個(gè)線程使用某一對(duì)象,避免不必要的參數(shù)傳遞;

          • 全鏈路追蹤中的traceId或者流程引擎中上下文的傳遞一般采用ThreadLocal;

          • Spring事務(wù)管理器采用了ThreadLocal;

          • Spring MVC的RequestContextHolder的實(shí)現(xiàn)使用了ThreadLocal;

          小結(jié)

          本篇文章我們從ThreadLocal的使用場(chǎng)景、源碼、類結(jié)構(gòu)、內(nèi)存結(jié)構(gòu)等進(jìn)行分析說明,最后分析了其引起內(nèi)存泄露的根本原因。通過本篇文章的學(xué)習(xí),基本上能掌握ThreadLocal百分之百九十的核心知識(shí)點(diǎn)。你學(xué)到了嗎?

          往期推薦

          來來來,聊聊7種內(nèi)存泄露場(chǎng)景和13種解決方案

          這一篇文章,可以把Java中的類加載器了解的七七八八了

          沒有監(jiān)控過JVM內(nèi)存的職場(chǎng)生涯,是不完美的

          基于SpringBoot實(shí)現(xiàn)讓日志像詩(shī)一樣有韻律

          與面試官聊try-catch-finally關(guān)閉資源,你的答案還是10年前的?



          如果你覺得這篇文章不錯(cuò),那么,下篇通常會(huì)更好。添加微信好友,可備注“加群”(微信號(hào):zhuan2quan)

          一篇文章就看透技術(shù)本質(zhì)的人,
            和花一輩子都看不清的人,
            注定是截然不同的搬磚生涯。
          ▲ 長(zhǎng)按關(guān)注”程序新視界“,洞察技術(shù)內(nèi)幕
          瀏覽 52
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲黄色小电影 | 国产三级片在线观看 | 黑人大屌与欧美成人视频 | 五月青春操 | 在线无码视频播放 |