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

來(lái)自:碼匠筆記


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

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

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

ThreadLocal 是什么?

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

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)容很簡(jiǎn)單,啟動(dòng) 3 個(gè)線程,分別調(diào)用 calc 方法,然后打印線程名字和計(jì)算內(nèi)容,輸出如下:

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

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

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

ThreadLocal 源碼賞析

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

當(dāng)然我們要尤為注意,key 是 this 也就是當(dāng)前的 ThreadLocal對(duì)象,記住這點(diǎn)下文要說(shuō)呢。

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 會(huì)內(nèi)存溢出嗎?

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

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

下面的例子中,在 i == 80 的時(shí)候做了一次強(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ì)停留在循環(huán)變量 i 等于 79 和 81 的地方,循環(huán) 100 次是為了更好的查看效果。好了我們可以直接觀察一下 i == 80 前后的運(yùn)行情況了

i == 79 || i == 81

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

Thread.currentThread().threadLocals

可以看到里面的 ThreadLocalMap 的 size 分別是 83 和 4,這說(shuō)明了什么?GC的時(shí)候把 83-4 = 79 個(gè) 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;
            }
        }

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

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

哎,如果 work1 的引用不在了,并且 Entry 對(duì) ThreadLocal 的引用是弱引用才會(huì)回收,是不是很巧妙的解決了這個(gè)問(wèn)題?


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

ThreadLocal 最佳實(shí)踐

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

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

接下來(lái)我們啟動(dòng)服務(wù),運(yùn)行我編寫(xiě)好的 Spring Boot Application

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

瀏覽器訪問(wèn) https://localhost:8080,瘋狂的刷新瀏覽器,控制臺(tái)打印的效果如下

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

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

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

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

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

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

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

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

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

長(zhǎng)按訂閱更多精彩▼

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

如有收獲,點(diǎn)個(gè)在看,誠(chéng)摯感謝

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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