聊一聊ThreadLocal
歡迎支持筆者新作:《深入理解Kafka:核心設(shè)計(jì)與實(shí)踐原理》和《RabbitMQ實(shí)戰(zhàn)指南》,同時(shí)歡迎關(guān)注筆者的微信公眾號(hào):朱小廝的博客。
歡迎跳轉(zhuǎn)到本文的原文鏈接:https://honeypps.com/java/thread-local-analysis/
對(duì)于ThreadLocal感興趣是從一個(gè)問(wèn)題開始的:ThreadLocal在何種情況下會(huì)發(fā)生內(nèi)存泄露?對(duì)于這個(gè)問(wèn)題的思考不得不去了解ThreadLocal本身的實(shí)現(xiàn)以及一些細(xì)節(jié)問(wèn)題等。接下去依次介紹ThreadLocal的功能,實(shí)現(xiàn)細(xì)節(jié),使用場(chǎng)景以及一些使用建議。
##概述
ThreadLocal不是用來(lái)解決對(duì)象共享訪問(wèn)問(wèn)題的,而主要提供了線程保持對(duì)象的方法和避免參數(shù)傳遞的方便的對(duì)象訪問(wèn)方式。一般情況下,通過(guò)ThreadLocal.set()到線程中的對(duì)象是該線程自己使用的對(duì)象,其他線程是不需要訪問(wèn)的,也訪問(wèn)不到的。各個(gè)線程中訪問(wèn)的是不同的對(duì)象。
ThreadLocal使用場(chǎng)合主要解決多線程中數(shù)據(jù)因并發(fā)產(chǎn)生不一致的問(wèn)題。ThreadLocal為每個(gè)線程的中并發(fā)訪問(wèn)的數(shù)據(jù)提供一個(gè)副本,通過(guò)訪問(wèn)副本來(lái)運(yùn)行業(yè)務(wù),這樣的結(jié)果是耗費(fèi)了內(nèi)存,但大大減少了線程同步所帶來(lái)的線程消耗,也介紹了線程并發(fā)控制的復(fù)雜度。
另外,說(shuō)ThreadLocal使得各線程能夠保持各自獨(dú)立的一個(gè)對(duì)象,并不是通過(guò)ThreadLocal.set()來(lái)實(shí)現(xiàn)的,而是通過(guò)每個(gè)線程中的new對(duì)象的操作來(lái)創(chuàng)建的對(duì)象,每個(gè)線程創(chuàng)建一個(gè),不是什么對(duì)象的拷貝或副本。通過(guò)ThreadLocal.set()將這個(gè)新創(chuàng)建的對(duì)象的引用保存到各線程的自己的一個(gè)map(Thread類中的ThreadLocal.ThreadLocalMap的變量)中,每個(gè)線程都有這樣一個(gè)map,執(zhí)行ThreadLocal.get()時(shí),各線程從自己的map中取出放進(jìn)去的對(duì)象,因此取出來(lái)的是各自自己線程中的對(duì)象,ThreadLocal實(shí)例是作為map的key來(lái)使用的。
【代碼1】
很多人會(huì)有這樣的無(wú)解:感覺(jué)這個(gè)ThreadLocal對(duì)象建立了一個(gè)類似于全局的map,然后每個(gè)線程作為map的key來(lái)存取對(duì)應(yīng)的線程本地的value。其實(shí)是ThreadLocal類中有一個(gè)ThreadLocalMap靜態(tài)內(nèi)部類,可以簡(jiǎn)單的理解為一個(gè)map,這個(gè)map為每個(gè)線程復(fù)制一個(gè)變量的“拷貝”存儲(chǔ)其中。下面是ThreadLocalMap的部分源碼:
【代碼2】
ThreadLocal類中一共有4個(gè)方法:
- T get()
- protected T initialValue()
- void remove()
- void set(T value)
就以get()方法為例
【代碼3】
get()方法的源碼如上所示,可以看到map中真正的key是線程ThreadLocal實(shí)例本身(ThreadLocalMap.Entry e = map.getEntry(this);中的this)。可以看一下getEntry(ThreadLocal key)的源碼.
【代碼4】
那么map中的value是什么呢?我們繼續(xù)來(lái)看源碼:
【代碼5】
代碼5中只能夠觀察到通過(guò)[protected T initialValue()]方法設(shè)置了一個(gè)初始值,當(dāng)然也可以通過(guò)set方法來(lái)賦值,繼續(xù)看源碼:
【代碼6】
ThreadLocal設(shè)置值有兩種方案:1. Override其initialValue方法;2. 通過(guò)set設(shè)置。
關(guān)于重寫initialValue方法可以參考下面這個(gè)例子簡(jiǎn)便的實(shí)現(xiàn):
【代碼7】
##內(nèi)存泄露
通過(guò)代碼1和代碼2的片段可以看出,在Thread類中保有ThreadLocal.ThreadLocalMap的引用,即在一個(gè)Java線程棧中指向了堆內(nèi)存中的一個(gè)ThreadLocal.ThreadLocalMap的對(duì)象,此對(duì)象中保存了若干個(gè)Entry,每個(gè)Entry的key(ThreadLocal實(shí)例)是弱引用,value是強(qiáng)引用(這點(diǎn)類似于WeakHashMap)。
用到弱引用的只是key,每個(gè)key都弱引用指向threadLocal,當(dāng)把threadLocal實(shí)例置為null以后,沒(méi)有任何強(qiáng)引用指向threadLocal實(shí)例,所以threadLocal將會(huì)被gc回收,但是value卻不能被回收,因?yàn)槠溥€存在于ThreadLocal.ThreadLocalMap的對(duì)象的Entry之中。只有當(dāng)前Thread結(jié)束之后,所有與當(dāng)前線程有關(guān)的資源才會(huì)被GC回收。所以,如果在線程池中使用ThreadLocal,由于線程會(huì)復(fù)用,而又沒(méi)有顯示的調(diào)用remove的話的確是會(huì)有可能發(fā)生內(nèi)存泄露的問(wèn)題。
其實(shí)在ThreadLocal.ThreadLocalMap的get或者set方法中會(huì)探測(cè)其中的key是否被回收(調(diào)用expungeStaleEntry方法),然后將其value設(shè)置為null,這個(gè)功能幾乎和WeakHashMap中的expungeStaleEntries()方法一樣。因此value在key被gc后可能還會(huì)存活一段時(shí)間,但最終也會(huì)被回收,但是若不再調(diào)用get或者set方法時(shí),那么這個(gè)value就在線程存活期間無(wú)法被釋放。
【代碼8】
其實(shí)ThreadLocal本身可以看成是沒(méi)有內(nèi)存泄露問(wèn)題的,通過(guò)顯示的調(diào)用remove方法即可。
##使用場(chǎng)景及方式
ThreadLocal的應(yīng)用場(chǎng)景,最適合的是按線程多實(shí)例(每個(gè)線程對(duì)應(yīng)一個(gè)實(shí)例)的對(duì)象的訪問(wèn),并且這個(gè)對(duì)象很多地方都要用到。
對(duì)于多線程資源共享的問(wèn)題,同步機(jī)制采用了“以時(shí)間換空間”的方式,比如定義一個(gè)static變量,同步訪問(wèn),而ThreadLocal采用了“以空間換時(shí)間”的方式。前者僅提供一份變量,讓不同的線程排隊(duì)訪問(wèn),而后者為每一個(gè)線程都提供了一份變量,因此可以同時(shí)訪問(wèn)而互不影響。
在多線程的開發(fā)中,經(jīng)常會(huì)考慮到的策略是對(duì)一些需要公開訪問(wèn)的屬性通過(guò)設(shè)置同步的方式來(lái)訪問(wèn)。這樣每次能保證只有一個(gè)線程訪問(wèn)它,不會(huì)有沖突。但是這樣做的結(jié)果會(huì)使得性能和對(duì)高并發(fā)的支持不夠。在某些情況下,如果我們不一定非要對(duì)一個(gè)變量共享不可,而是給每個(gè)線程一個(gè)這樣的資源副本,讓他們可以獨(dú)立都各自跑各自的,這樣不是可以大幅度的提高并行度和性能了嗎?
還有的情況是有的數(shù)據(jù)本身不是線程安全的,或者說(shuō)它只能被一個(gè)線程使用,不能被其它線程同時(shí)使用。如果等一個(gè)線程使用完了再給另一個(gè)線程使用就根本不現(xiàn)實(shí)。這樣的情況下,我們也可以考慮用ThreadLocal。
ThreadLocal建議:
##InheritableThreadLocal
InheritableThreadLocal是ThreadLocal的子類,代碼量很少,可以看一下:
【代碼9】
這里主要的還是一個(gè)childValue這個(gè)方法。
在代碼7中示范了ThreadLocal的方法,而使用類InheritableThreadLocal可以在子線程中取得父線程繼承下來(lái)的值。可以采用重寫childValue(Object parentValue)方法來(lái)更改繼承的值。
查看案例:
【代碼10】
運(yùn)行結(jié)果:
Main: get value = 1467100984858 Thread-0: get value = 1467100984858 which plus in subThread.如果去掉@Override protected Object childValue(Object parentValue)方法運(yùn)行結(jié)果:
Main: get value = 1461585396073 Thread-0: get value = 1461585396073參考資料
歡迎跳轉(zhuǎn)到本文的原文鏈接:https://honeypps.com/java/thread-local-analysis/
歡迎支持筆者新作:《深入理解Kafka:核心設(shè)計(jì)與實(shí)踐原理》和《RabbitMQ實(shí)戰(zhàn)指南》,同時(shí)歡迎關(guān)注筆者的微信公眾號(hào):朱小廝的博客。
總結(jié)
以上是生活随笔為你收集整理的聊一聊ThreadLocal的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Java多线程知识小抄集(四)——完结
- 下一篇: 这里有一份面筋请查收(二)