當(dāng)前位置:首頁 > 公眾號精選 > 架構(gòu)師社區(qū)
[導(dǎo)讀]來自:碼匠筆記 ThreadLocal 對于大家并不陌生,每個人應(yīng)該多少都用過,或者接觸過,那么你真的了解她嗎?我也是今天才揭開了她的神秘面紗。 看完這篇文章你將 GET 如下知識點(diǎn): 什么是 ThreadLocal? ThreadLocal 真的會導(dǎo)致內(nèi)存溢出嗎? ThreadLocal 源碼淺

來自:碼匠筆記


ThreadLocal對于大家并不陌生,每個人應(yīng)該多少都用過,或者接觸過,那么你真的了解她嗎?我也是今天才揭開了她的神秘面紗。

看完這篇文章你將 GET 如下知識點(diǎn):

  1. 什么是 ThreadLocal?
  2. ThreadLocal 真的會導(dǎo)致內(nèi)存溢出嗎?
  3. ThreadLocal 源碼淺析
  4. ThreadLocal 最佳實(shí)踐
  5. ThreadLocal.remove 解決的到底是什么問題?

ThreadLocal 是什么?

ThreadLocal字面意思是線程本地變量,那么什么是線程本地變量呢?他解決了什么問題?先看下面這個例子

public class ThreadLocalTest { public static void main(String[] args) {
        Task task = new Task(); for (int i = 0; i < 3; i++) { new Thread(() -> System.out.println(Thread.currentThread().getName() + " : " + task.calc(10))).start();
        }
    } static class Task { private int value; public int calc(int i) {
            value += i; return value;
        }
    }
}

內(nèi)容很簡單,啟動 3 個線程,分別調(diào)用 calc 方法,然后打印線程名字和計算內(nèi)容,輸出如下:

Thread-0 : 10
Thread-1 : 20
Thread-2 : 30

結(jié)果不難分析,因?yàn)檫@么 3 個線程共用一個 Task對象,所以 value 內(nèi)容會累加,那么結(jié)果是不是不是你預(yù)期呢?那么我們再看一個例子

public class ThreadLocalTest2 { public static void main(String[] args) {
        ThreadLocalTest2.Task task = new ThreadLocalTest2.Task(); for (int i = 0; i < 3; i++) { new Thread(() -> System.out.println(Thread.currentThread().getName() + " : " + task.calc(10))).start();
        }
    } static class Task {
        ThreadLocalvalue; public int calc(int i) {
            value = new ThreadLocal();
            value.set((value.get() == null ? 0 : value.get()) + i); return value.get();
        }
    }
}

運(yùn)行結(jié)果如下

Thread-0 : 10
Thread-1 : 10
Thread-2 : 10

這次結(jié)果就對了吧,把 value 修改成了 ThreadLocal,然后每個線程就不會互相影響內(nèi)容了,那么為什么他可以做到呢?這就是  ThreadLocal的意義所在,他解決的就是線程私有變量,多線程不互相影響。我們?nèi)ピ创a一看究竟

ThreadLocal 源碼賞析

看源碼最簡單粗暴的方式就是從入口進(jìn)行,我們直接看 ThreadLocal.set方法,她直接獲取了當(dāng)前線程,然后調(diào)用了 getMap(t),也就是當(dāng)前線程的 threadLocals變量,如果沒有直接調(diào)用 createMap創(chuàng)建,然后返回,那么看到這里我們就知道了,ThreadLocal就是一個工具類,讓我們可以把內(nèi)容通過k-v的方式設(shè)置在當(dāng)前線程上面(里面實(shí)際是使用 ThreadLocalMap進(jìn)行存儲,秒看一下代碼和 HashMap 原理非常相似),既然存儲在當(dāng)前線程上面那么當(dāng)然不會有線程安全問題了,這就是線程本地變量的內(nèi)容嘍。

當(dāng)然我們要尤為注意,key 是 this 也就是當(dāng)前的 ThreadLocal對象,記住這點(diǎ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);
  }
ThreadLocalMap getMap(Thread t) { return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
   t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocal 會內(nèi)存溢出嗎?

不過還沒有結(jié)束,大家最愛談了的就是 ThreadLocal 的內(nèi)存溢出問題,那么她真的會內(nèi)存溢出么?

我們再看一個例子,例子和剛才不同的地方是只使用了一個線程(也就是 Main 線程)循環(huán)運(yùn)行示例,每次創(chuàng)建新的 Task 對象,我們可想而知,這樣每次創(chuàng)建不同的 Task,只要線程不結(jié)束,會不停的往當(dāng)前線程的 threadLocals里面 set 內(nèi)容,因?yàn)槊看味际切?Task ,自然 ThreadLocal也是新的,那么如果循環(huán)足夠大,并且線程一直存在,肯定會內(nèi)存溢出呢呀?。?!我們自己動手試試才知道啊。

下面的例子中,在 i == 80 的時候做了一次強(qiáng)制 GC,我們直接 DEBUG 看下效果。

public class ThreadLocalTest3 { public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Task().calc(10); if (i == 80) {
                System.gc();
            }
        }
    } static class Task {
        ThreadLocalvalue; public int calc(int i) {
            value = new ThreadLocal();
            value.set((value.get() == null ? 0 : value.get()) + i); return value.get();
        }
    }
}

在 for 循環(huán)行的左側(cè)點(diǎn)擊 debug,然后點(diǎn)擊右鍵,這樣 DEBUG 會停留在循環(huán)變量 i 等于 79 和 81 的地方,循環(huán) 100 次是為了更好的查看效果。好了我們可以直接觀察一下 i == 80 前后的運(yùn)行情況了

i == 79 || i == 81

那么開始我的表演,DEBUG 分別停在了 79 和 81 的位置上面,我們直接運(yùn)行一下當(dāng)前線程的內(nèi)容獲取到 threadLocals的內(nèi)容

Thread.currentThread().threadLocals

可以看到里面的 ThreadLocalMap 的 size 分別是 83 和 4,這說明了什么?GC的時候把 83-4 = 79 個 ThreadLocalMap的內(nèi)容回收了?


好吧,那我們繼續(xù)看下代碼吧

private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table; int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked")
                    ThreadLocalkey = (ThreadLocal) e.get(); if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }
static class Entry extends WeakReference<ThreadLocal> { /** The value associated with this ThreadLocal. */ Object value;

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

原來 ThreadLocal的 ThreadLocalMap里面存的每一個 Entry 是一個 WeakReference,WeakReference會在 GC 的時候進(jìn)行回收,回收的其實(shí)是 key,也就是弱引用的 referent, 然后  ThreadLocal會在 set 和 get 的時候?qū)?key 為空的 value 進(jìn)行刪除,所以這樣就完美解決了當(dāng)前線程生命周期不結(jié)束的時候,不同的 ThreadLocal不停的追加到當(dāng)前線程上面,導(dǎo)致內(nèi)存溢出。

等等,那我自己寫個程序,遇到 GC 不是就獲取不到 ThreadLocal對象了嗎?不是的,因?yàn)橐粋€對象只有僅僅被 WeakReference引用才會被回收。

哎,如果 work1 的引用不在了,并且 Entry 對 ThreadLocal 的引用是弱引用才會回收,是不是很巧妙的解決了這個問題?


所以 WeakReference解決的就是內(nèi)存溢出問題,如果持有 ThreadLocal 對象被回收了,內(nèi)存自然會被回收,如果 ThreadLocal 的對象一直存在不被回收,并不能稱之為內(nèi)存溢出。

ThreadLocal 最佳實(shí)踐

千呼萬喚始出來,因?yàn)?ThreadLocal這個特性,深受各種框架喜歡,比如 MyBatis,Spring 大量的使用的 ThreadLocal,下面是用一個最常用的案例說明一下,首先我有一個攔截器,每次請求來,使用當(dāng)前的 sl 的內(nèi)容 + 10,我是為了模擬效果,通常這個做法是用于傳遞當(dāng)前登錄態(tài),以便一次請求在任何地方都可以輕松的獲取到登錄態(tài)。

public class SessionInterceptor implements HandlerInterceptor { public static ThreadLocalsl = new ThreadLocal(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        Integer value = sl.get(); if (value != null) {
            sl.set(value + 10);
        } else {
            sl.set(10);
        } return true;
    } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {

    } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {

    }
}

然后我在 controller 里面獲取 ThreadLocal里面的內(nèi)容,并打印當(dāng)前線程的名稱和內(nèi)容

@RestController public class IndexController { @RequestMapping("/") public Integer test() {
        System.out.println(Thread.currentThread().getName() + " : " + SessionInterceptor.sl.get()); return SessionInterceptor.sl.get();
    }
}

接下來我們啟動服務(wù),運(yùn)行我編寫好的 Spring Boot Application

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) public class MainBootstrap { public static void main(String[] args) {
        SpringApplication.run(MainBootstrap.class, args);
    }
}

瀏覽器訪問 https://localhost:8080,瘋狂的刷新瀏覽器,控制臺打印的效果如下

http-nio-8080-exec-1 : 10
http-nio-8080-exec-3 : 10
http-nio-8080-exec-4 : 10
http-nio-8080-exec-1 : 20
http-nio-8080-exec-2 : 10
http-nio-8080-exec-3 : 20
http-nio-8080-exec-4 : 20
http-nio-8080-exec-1 : 30
http-nio-8080-exec-2 : 20
http-nio-8080-exec-3 : 30
http-nio-8080-exec-4 : 30

呀,和我想象的不一樣啊,我這可是瀏覽器的請求,不應(yīng)該是每個請求一個線程,使用自己的 ThreadLocal 嗎,怎么值也累加了?

別慌問題出現(xiàn)在這里,在池化技術(shù)流行的年代,自然 Tomcat 也用了池化基礎(chǔ),其實(shí)每個請求過來,是直接在 Tomcat 的線程池里面獲取一個線程,然后運(yùn)行,所以一個請求結(jié)束如果 ThreadLocal 的內(nèi)容不重置,就會影響其他請求,想象如果你這個地方是做的用戶登錄的綁定,那么豈不是資源亂套了?

那么怎么解決呢?還記得剛才的 SessionInterceptor 類么,直接在里面的 afterCompletion添加 sl.remove()即可,意思是在請求結(jié)束的時候,把當(dāng)前線程的私有變量刪除,這樣就不影響其他線程了。

網(wǎng)上的一些說這個操作是為了更好的 GC 回收沒用的實(shí)例,如果不設(shè)置也會自動回收,其實(shí)是不對的。為了讓上下文都可以獲取到 ThreadLocal 的內(nèi)容,所以比如是靜態(tài)的 ThreadLocal 所以持有的引用一直存在,并不會被回收,所以其實(shí)是在恢復(fù)線程的狀態(tài),不影響其他請求。

@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        sl.remove();
    }

修改以后我們重新狂刷瀏覽器,是不是問題就解決了呢?好的如果你有任何關(guān)于 ThreadLocal 的問題歡迎給我留言其他討論,如果有不對的地方也歡迎指正。對了所有文章中的代碼,都可以在訂閱號后臺回復(fù) “ThreadLocal” 獲取。

特別推薦一個分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長按關(guān)注一下:

用了三年 ThreadLocal 今天才弄明白其中的道理

長按訂閱更多精彩▼

用了三年 ThreadLocal 今天才弄明白其中的道理

如有收獲,點(diǎn)個在看,誠摯感謝

免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉(zhuǎn)型技術(shù)解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關(guān)鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時企業(yè)卻面臨越來越多業(yè)務(wù)中斷的風(fēng)險,如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競爭力 堅(jiān)持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競爭優(yōu)勢...

關(guān)鍵字: 通信 BSP 電信運(yùn)營商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術(shù)學(xué)會聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(shù)(集團(tuán))股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉