JDK源碼白話解讀之ThreadLocal篇
引言
因此本文主要結(jié)合常見的一些疑問(wèn)、ThreadLocal
源碼、應(yīng)用實(shí)例以注意事項(xiàng)來(lái)全面而深入地再詳細(xì)講解一遍ThreadLocal
。希望大家看完本文后可以徹底掌握ThreadLocal
。
ThreadLocal是什么?它能干什么?
在闡述ThreadLocal
之前,我們先來(lái)看下它的設(shè)計(jì)者是怎么描述ThreadLocal
的吧。
看完官方的描述后,結(jié)合自己的理解,ThreadLocal
提供了一種對(duì)應(yīng)獨(dú)立線程內(nèi)的數(shù)據(jù)訪問(wèn)機(jī)制,實(shí)現(xiàn)了變量在線程之間隔離,在線程生命周期內(nèi)獨(dú)立獲取或者設(shè)置的能力。如果我們想在線程內(nèi)傳遞參數(shù)但是有不想作為方法參數(shù)的時(shí)候,ThreadLocal
就可以排上用場(chǎng)了。不過(guò)值得注意的是ThreadLocal
并不會(huì)解決變量共享問(wèn)題。實(shí)際上從ThreadLocal
的名稱上面來(lái)看,線程本地變量也已經(jīng)大致說(shuō)明了它的作用,所以變量的命名還是非常重要的,要做到顧名思義。如果覺得還不是很理解,沒關(guān)系,我們可以通過(guò)以下的場(chǎng)景再加深下理解。
假如有以下的場(chǎng)景,假設(shè)只有一個(gè)數(shù)據(jù)庫(kù)連接,客戶端1、2、3都需要獲取數(shù)據(jù)庫(kù)連接來(lái)進(jìn)行具體的數(shù)據(jù)庫(kù)操作,但是同一時(shí)間點(diǎn)只能有一個(gè)線程獲取連接,其他線程只能等待。因此就會(huì)出現(xiàn)數(shù)據(jù)庫(kù)訪問(wèn)效率不高的問(wèn)題。
那我們有沒有什么辦法能夠避免線程等待的情況呢?上述問(wèn)題的根本原因是數(shù)據(jù)庫(kù)連接是共享變量,同事只能有一個(gè)線程可以進(jìn)行操作。那如果三個(gè)線程都有自己的數(shù)據(jù)庫(kù)連接,互相隔離,那不就不會(huì)出現(xiàn)等待的問(wèn)題了嘛。那么此時(shí)我么可以使用ThreadLocal
實(shí)現(xiàn)在不同線程中的變量隔離。可以看出來(lái),ThreadLocal
是一種已空間換取時(shí)間的做法。
ThreadLocal實(shí)現(xiàn)線程隔離的秘密
從上文中,我們了解到ThreadLocal
可以實(shí)現(xiàn)變量訪問(wèn)的線程級(jí)別的隔離。那么它是到底如何實(shí)現(xiàn)的呢?這還需要結(jié)合Thread
以及ThreadLocal
的源碼來(lái)分析才能揭開ThreadLocal
實(shí)現(xiàn)線程隔離的神秘面紗。
public class Thread implements Runnable { ... /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; ... }
在Thread
源碼中我們發(fā)現(xiàn),它有一個(gè)threadLocals
變量,它的類型是ThreadLocal中的內(nèi)部類ThreadLocalMap
。我們?cè)诳聪?code>ThreadLocalMap的定義是怎樣的。從源碼中我們可以看出來(lái),ThreadLocalMap
實(shí)際上就是Entry
數(shù)組,這個(gè)Entry
對(duì)應(yīng)的key
實(shí)際就是ThreadLocal
的實(shí)例,value
就是實(shí)際的變量值。
public class ThreadLocal<T> { ... 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; } } ... //底層數(shù)據(jù)結(jié)構(gòu)是數(shù)組 private Entry[] table; ... } ... }
通過(guò)查看上述的源碼,如果還不太好理解的話,我們?cè)俳Y(jié)合下現(xiàn)實(shí)中的例子來(lái)理解。大家都有支付寶賬戶,我們通過(guò)它來(lái)管理著我們的銀行卡、余額、花唄這些金融服務(wù)。
我們以支付寶以及支付寶賬戶進(jìn)行類比,假設(shè)ThreadLocal
就是支付寶,每個(gè)支付寶賬戶實(shí)際就是單獨(dú)的線程,而賬戶中的余額屬性就相當(dāng)于Thread
的私有屬性ThreadLocalMap
。我們?cè)谌粘I钪?,進(jìn)行賬戶余額的充值或者消費(fèi),并不是直接通過(guò)賬戶進(jìn)行操作的,而是借助于支付寶進(jìn)行維護(hù)的。這就相當(dāng)于每個(gè)線程對(duì)ThreadLocalMap
進(jìn)行操作的時(shí)候也不是直接操作的,而是借助于ThreadLocal
來(lái)操作。
那么Thread
到底是怎么借助ThreadLocal
進(jìn)行私有屬性管理的呢?還是需要進(jìn)一步查看Thread
進(jìn)行set
以及get
操作的源碼。從以下的ThreadLocal
的源碼中我們可以看出,在進(jìn)行操作之前,需要獲取當(dāng)前的執(zhí)行操作的線程,再根據(jù)線程或者線程中私有的ThreadLocalMap
屬性來(lái)進(jìn)行操作。
在進(jìn)行數(shù)據(jù)獲取的時(shí)候,也是按照同樣的流程,先獲取當(dāng)前的線程,再獲取線程中對(duì)應(yīng)的ThreadLocalMap
屬性來(lái)進(jìn)行后續(xù)的值的獲取。
經(jīng)過(guò)上述的源碼的分析,我們可以得出這樣的結(jié)論,ThreadLocal
之所以可以實(shí)現(xiàn)變量的線程隔離訪問(wèn),實(shí)際上就是借助于Thread
中的ThreadLocalMap
屬性來(lái)進(jìn)行操作。由于都是操作線程本身的屬性,因此并不會(huì)影響其他線程中的變量值,因此可以實(shí)現(xiàn)線程級(jí)別的數(shù)據(jù)修改隔離。
為什么ThreadLocal會(huì)出現(xiàn)OOM的問(wèn)題?
內(nèi)存泄漏演示
我們都知道,ThreadLocal
如果使用不當(dāng)?shù)脑挄?huì)出現(xiàn)內(nèi)存泄漏的問(wèn)題,那么我們就通過(guò)下面的這段代碼來(lái)分析下,內(nèi)存泄漏的原因到底是什么。
/** * @author mufeng * @description 測(cè)試ThreadLocal內(nèi)存溢出 * @date 2022/1/16 19:01 * @since */ public class ThreadLocalOOM { /** * 測(cè)試線程池 */ private static Executor threadPool = new ThreadPoolExecutor(3, 3, 40, TimeUnit.SECONDS, new LinkedBlockingDeque<>()); static class Info { private byte[] info = new byte[10 * 1024 * 1024]; } private static ThreadLocal<Info> infoThreadLocal = new ThreadLocal<>(); public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; i++) { threadPool.execute(() -> { infoThreadLocal.set(new Info()); System.out.println("Thread started:" + Thread.currentThread().getName()); }); Thread.sleep(100); } } }
手動(dòng)進(jìn)行GC
之后,我們可以發(fā)現(xiàn)堆中仍然有超過(guò)30M的堆內(nèi)存占用,如上面的代碼,在線程池中活躍的線程會(huì)有三個(gè),對(duì)應(yīng)的value
為10M,說(shuō)明在線程還存活的情況下,對(duì)應(yīng)的value
并沒有被回收,因此存在內(nèi)存泄漏的情況,如果存在大量線程的情況,就會(huì)出現(xiàn)OOM
。
當(dāng)我們修改代碼在線程中進(jìn)行remove
操作,手動(dòng)GC之后我們發(fā)現(xiàn)堆內(nèi)存趨近于0了,之前沒有被回收的對(duì)象已經(jīng)被回收了。
內(nèi)存泄漏問(wèn)題分析
以上是對(duì)于ThreadLocal
發(fā)生內(nèi)存泄漏問(wèn)題的演示,那么再來(lái)仔細(xì)分析下背后的原因是什么。ThreadLocal
中實(shí)際存儲(chǔ)數(shù)據(jù)的是ThreadLocalMap
,實(shí)際上Map
對(duì)應(yīng)的key
是一個(gè)虛引用,在GC
的時(shí)候可以被回收掉,但是問(wèn)題就在于key所對(duì)應(yīng)的value
,它是強(qiáng)引用,只要線程存活,那么這條引用鏈就會(huì)一致存在,如果出現(xiàn)大量線程的時(shí)候就會(huì)有OOM
的風(fēng)險(xiǎn)。 所以在使用ThreadLocal
的時(shí)候一定記得要顯式的調(diào)用remove
方法進(jìn)行清理,防止內(nèi)存泄漏。
父子線程的參數(shù)傳遞
到這里,我相信大家對(duì)于ThreadLocal
的原理有了比較深入的理解了。結(jié)合上文中的ThreadLocal
代碼,不知道大家有沒有思考過(guò)一個(gè)問(wèn)題,我們?cè)谑褂?code>ThreadLocal的時(shí)候都是在同一個(gè)線程內(nèi)進(jìn)行了set
以及get
操作,那么如果set
操作與get
操作在父子線程中是否還可以正常的獲取呢?帶著這樣的疑問(wèn),我們來(lái)看下如下的代碼。
/** * @author mufeng * @description 父子線程參數(shù)傳遞 * @date 2022/1/16 9:54 * @since */ public class InheritableThreadLocalMain { private static final ThreadLocal<String> count = new ThreadLocal<>(); public static void main(String[] args) { count.set("父子線程參數(shù)傳遞?。?!"); System.out.println(Thread.currentThread().getName() + ":" + count.get()); new Thread(() -> { System.out.println(Thread.currentThread().getName() + ":" + count.get()); }).start(); } }
與之前代碼有所不同,ThreadLocal的設(shè)值是在main線程中進(jìn)行的,但是獲取操作實(shí)際是在主線程下的子線程中進(jìn)行的,大家可以分析一下運(yùn)行結(jié)果是怎么樣的。
看到這個(gè)運(yùn)行結(jié)果,不知道大家分析的對(duì)不對(duì)呢。實(shí)際上如果理解了上文的核心的話,這個(gè)問(wèn)題應(yīng)該很好分析的。ThreadLocal
獲取數(shù)據(jù)的時(shí)候,首先是需要獲取當(dāng)前的線程的,根據(jù)線程獲取實(shí)際存儲(chǔ)數(shù)據(jù)的ThreadLocalMap
,上文代碼中設(shè)置和獲取在父子線程中進(jìn)行,那肯定是獲取不到設(shè)置的數(shù)據(jù)的。但是在現(xiàn)實(shí)的項(xiàng)目開發(fā)中,我們會(huì)經(jīng)常遇到需要將父線程的變量值傳遞給子線程進(jìn)行處理,那么應(yīng)該要怎么來(lái)實(shí)現(xiàn)呢?這個(gè)時(shí)候InheritableThreadLocal
就派上用場(chǎng)了。
/** * @author mufeng * @description 父子線程參數(shù)傳遞 * @date 2022/1/16 9:54 * @since */ public class InheritableThreadLocalMain { private static final ThreadLocal<String> count = new InheritableThreadLocal<>(); public static void main(String[] args) { count.set("父子線程參數(shù)傳遞?。?!"); System.out.println(Thread.currentThread().getName() + ":" + count.get()); new Thread(() -> { System.out.println(Thread.currentThread().getName() + ":" + count.get()); }).start(); } }
那么InheritableThreadLocal
到底是如何實(shí)現(xiàn)父子線程的參數(shù)傳遞的呢?我么還是的看看源碼中的實(shí)現(xiàn)原理。實(shí)際上在Thread
源碼中,除了有Threadlocal
私有屬性還有InheritableThreadLocal
私有屬性。
public class Thread implements Runnable { /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; ... public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } private void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null, true); } private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { ... //關(guān)鍵 if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); ... } ... }
實(shí)際在進(jìn)行子線程創(chuàng)建的時(shí)候,在線程初始化過(guò)程中,判斷了父線程中的inheritableThreadLocals
屬性是否為空,如果不為空的話需要進(jìn)行值的復(fù)制,這樣便實(shí)現(xiàn)了父子線程的值傳遞。
總結(jié)
本文主要對(duì)ThreadLocal
進(jìn)行了相對(duì)全面的分析,從它的使用場(chǎng)景、原理以及源碼分析、產(chǎn)生OOM
的原因以及一些使用上的注意,相信通過(guò)本文的學(xué)習(xí),大家對(duì)于ThreadLocal
會(huì)有更加深刻的理解。
到此這篇關(guān)于JDK源碼白話解讀之ThreadLocal篇的文章就介紹到這了,更多相關(guān)Java ThreadLocal內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳談jvm--Java中init和clinit的區(qū)別
下面小編就為大家?guī)?lái)一篇詳談jvm--Java中init和clinit的區(qū)別。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10IDEA上面搭建一個(gè)SpringBoot的web-mvc項(xiàng)目遇到的問(wèn)題
這篇文章主要介紹了IDEA上面搭建一個(gè)SpringBoot的web-mvc項(xiàng)目遇到的問(wèn)題小結(jié),需要的朋友可以參考下2017-04-04Java創(chuàng)建類模式_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了Java創(chuàng)建類模式的相關(guān)方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08簡(jiǎn)單易懂Java反射的setAccessible()方法
本文主要介紹了簡(jiǎn)單易懂Java反射的setAccessible()方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07ArrayList?foreach循環(huán)增添刪除導(dǎo)致ConcurrentModificationException解決分
這篇文章主要為大家介紹了ArrayList?foreach循環(huán)增添刪除導(dǎo)致ConcurrentModificationException解決分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪<BR>2023-12-12