Java中的線程私有變量ThreadLocal詳解
什么是ThreadLocal
首先看下ThreadLocal的使用示例:
public class ThreadLocalTest { private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { Thread thread1 = new Thread(() -> { threadLocal.set("本地變量1"); print("thread1"); System.out.println("線程1的本地變量的值為:"+threadLocal.get()); }); Thread thread2 = new Thread(() -> { threadLocal.set("本地變量2"); print("thread2"); System.out.println("線程2的本地變量的值為:"+threadLocal.get()); }); thread1.start(); thread2.start(); } public static void print(String s){ System.out.println(s+":"+threadLocal.get()); }
執(zhí)行結(jié)果如下
我們從 Thread
類講起,在 Thread
類中有維護(hù)兩個(gè) ThreadLocal.ThreadLocalMap
對象,分別是: threadLocals
和 inheritableThreadLocals
。
/* 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;
初始它們都為 null,只有在調(diào)用 ThreadLocal
類的 set 或 get 時(shí)才創(chuàng)建它們。ThreadLocalMap可以理解為線程私有的HashMap。
ThreadLoalMap是ThreadLocal中的一個(gè)靜態(tài)內(nèi)部類,類似HashMap的數(shù)據(jù)結(jié)構(gòu),但并沒有實(shí)現(xiàn)Map接口。
ThreadLoalMap中初始化了一個(gè)大小16的Entry數(shù)組,Entry對象用來保存每一個(gè)key-value鍵值對。key是ThreadLocal對象。
Entry用來保存數(shù)據(jù) ,而且還是繼承的弱引用。在Entry內(nèi)部使用ThreadLocal作為key,使用我們設(shè)置的value作為value。
ThreadLocal 原理
set()方法
當(dāng)我們調(diào)用 ThreadLocal 的 set()
方法時(shí)實(shí)際是調(diào)用了當(dāng)前線程的 ThreadLocalMap 的 set() 方法。
ThreadLocal 的 set() 方法中,會(huì)進(jìn)一步調(diào)用 Thread.currentThread()
獲得當(dāng)前線程對象 ,然后獲取到當(dāng)前線程對象的ThreadLocal,判斷是不是為空,為空就先調(diào)用 creadMap()
創(chuàng)建再 set(value)
創(chuàng)建 ThreadLocalMap 對象并添加變量。不為空就直接 set(value)
。
這種保證線程安全的方式稱為 線程封閉
。線程只能看到自己的ThreadLocal變量。線程之間是互相隔離的。
get()方法
其中get()方法用來獲取與當(dāng)前線程關(guān)聯(lián)的ThreadLocal的值,如果當(dāng)前線程沒有該ThreadLocal的值,則調(diào)用initialValue函數(shù)獲取初始值返回
所以一般我們使用時(shí)需要繼承該函數(shù),給出初始值(不重寫的話默認(rèn)返回Null)。
主要有以下幾步:
- 獲取當(dāng)前的Thread對象,通過getMap獲取Thread內(nèi)的ThreadLocalMap
- 如果map已經(jīng)存在,以當(dāng)前的ThreadLocal為鍵,獲取Entry對象,并從從Entry中取出值
- 否則,調(diào)用setInitialValue進(jìn)行初始化。
/** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */ 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(); }
我們可以重寫 initialValue()
,設(shè)置初始值。
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){ @Override protected Integer initialValue() { return Integer.valueOf(0); } }
remove()方法
最后一個(gè)需要探究的就是remove方法,它用于在map中移除一個(gè)不用的Entry。也是先計(jì)算出hash值,若是第一次沒有命中,就循環(huán)直到null,在此過程中也會(huì)調(diào)用expungeStaleEntry清除空key節(jié)點(diǎn)。代碼如下:
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } /** * Remove the entry for key. */ private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }
實(shí)際上 ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱引用,弱引用的特點(diǎn)是,如果這個(gè)對象只存在弱引用,那么在下一次垃圾回收的時(shí)候必然會(huì)被清理掉。
所以如果 ThreadLocal 沒有被外部強(qiáng)引用的情況下,在垃圾回收的時(shí)候會(huì)被清理掉的,這樣一來 ThreadLocalMap中使用這個(gè) ThreadLocal 的 key 也會(huì)被清理掉。但是,value 是強(qiáng)引用,不會(huì)被清理,這樣一來就會(huì)出現(xiàn) key 為 null 的 value。出現(xiàn)內(nèi)存泄漏的問題。
在執(zhí)行 ThreadLocal 的 set、remove、rehash 等方法時(shí),它都會(huì)掃描 key 為 null 的 Entry,如果發(fā)現(xiàn)某個(gè) Entry 的 key 為 null,則代表它所對應(yīng)的 value 也沒有作用了,所以它就會(huì)把對應(yīng)的 value 置為 null,這樣,value 對象就可以被正?;厥樟恕5羌僭O(shè) ThreadLocal 已經(jīng)不被使用了,那么實(shí)際上 set、remove、rehash 方法也不會(huì)被調(diào)用,與此同時(shí),如果這個(gè)線程又一直存活、不終止的話,那么剛才的那個(gè)調(diào)用鏈就一直存在,也就導(dǎo)致了 value 的內(nèi)存泄漏。
ThreadLocal 的Hash算法
ThreadLocalMap
類似HashMap,它有自己的Hash算法。
private final int threadLocalHashCode = nextHashCode(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); }
HASH_INCREMENT
這個(gè)數(shù)字被稱為斐波那契數(shù) 也叫 黃金分割數(shù),帶來的好處就是 hash
分布非常均勻。
每當(dāng)創(chuàng)建一個(gè) ThreadLocal
對象,這個(gè) ThreadLocal.nextHashCode
這個(gè)值就會(huì)增長 0x61c88647
。
講到Hash就會(huì)涉及到Hash沖突,跟HashMap通過鏈地址法不同的是,ThreadLocal是通過線性探測法/開放地址法來解決hash沖突。
ThreadLocal 1.7和1.8的區(qū)別
ThreadLocal 1.7版本的時(shí)候,entry對象的key是Thread。
1.8版本entry的key是ThreadLocal。
1.8版本的好處 :當(dāng)Thread銷毀的時(shí)候,ThreadLocalMap也會(huì)隨之銷毀,減少內(nèi)存的使用。因?yàn)門hreadLocalMap是在Thread里面的,所以只要Thread消失了,那ThreadLocalMap就不復(fù)存在了。
ThreadLocal 的問題
ThreadLocal 內(nèi)存泄露問題
在 ThreadLocalMap 中的 Entry 的 key 是對 ThreadLocal 的 WeakReference
弱引用,而 value 是強(qiáng)引用。
當(dāng) ThreadLocalMap 的某 ThreadLocal 對象只被弱引用,GC 發(fā)生時(shí)該對象會(huì)被清理,此時(shí) key 為 null,但 value 為強(qiáng)引用不會(huì)被清理。
此時(shí) value 將訪問不到也不被清理掉就可能會(huì)導(dǎo)致內(nèi)存泄漏。
注意構(gòu)造函數(shù)里的第一行代碼super(k),這意味著ThreadLocal對象是一個(gè)弱引用
/** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
因此我們使用完 ThreadLocal 后最好手動(dòng)調(diào)用 remove()
方法。但其實(shí)在 ThreadLocalMap 的實(shí)現(xiàn)中以及考慮到這種情況,因此在調(diào)用 set()
、 get()
、 remove()
方法時(shí),會(huì)清理 key 為 null 的記錄。
為什么采用了弱引用的實(shí)現(xiàn)而不是強(qiáng)引用呢?
注釋上有這么一段話:為了協(xié)助處理數(shù)據(jù)比較大并且生命周期比較長的場景,hash table的條目使用了WeakReference作為key。
所以,弱引用反而是為了解決內(nèi)存存儲(chǔ)問題而專門使用的。
實(shí)際上,采用弱引用反而多了一層保障,ThreadLocal被清理后key為null,對應(yīng)的value在下一次ThreadLocalMap調(diào)用set、get,就算忘記調(diào)用 remove 方法,弱引用比強(qiáng)引用可以多一層保障。
所以,內(nèi)存泄露的根本原因是是否手動(dòng)清除操作,而不是弱引用。
ThreadLocal 父子線程繼承
異步場景下無法給子線程共享父線程的線程副本數(shù)據(jù),可以通過 InheritableThreadLocal
類解決這個(gè)問題。
它的原理就是子線程是通過在父線程中調(diào)用 new Thread()
創(chuàng)建的,在 Thread 的構(gòu)造方法中調(diào)用了 Thread的init
方法,在 init
方法中父線程數(shù)據(jù)會(huì)復(fù)制到子線程( ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
)。
代碼示例:
public class InheritableThreadLocalDemo { public static void main(String[] args) { ThreadLocal<String> threadLocal = new ThreadLocal<>(); ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>(); threadLocal.set("父類數(shù)據(jù):threadLocal"); inheritableThreadLocal.set("父類數(shù)據(jù):inheritableThreadLocal"); new Thread(new Runnable() { @Override public void run() { System.out.println("子線程獲取父類threadLocal數(shù)據(jù):" + threadLocal.get()); System.out.println("子線程獲取父類inheritableThreadLocal數(shù)據(jù):" +inheritableThreadLocal.get()); } }).start(); } }
但是我們做異步處理都是使用線程池,線程池會(huì)復(fù)用線程會(huì)導(dǎo)致問題出現(xiàn)。我們可以使用阿里巴巴的TTL解決這個(gè)問題。
阿里巴巴的TTL:
https://github.com/alibaba/transmittable-thread-local
到此這篇關(guān)于Java中的線程私有變量ThreadLocal詳解的文章就介紹到這了,更多相關(guān)Java線程ThreadLocal內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot打包不同環(huán)境配置以及shell腳本部署的方法
這篇文章主要給大家介紹了關(guān)于springboot打包不同環(huán)境配置以及shell腳本部署的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者使用springboot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03詳解Spring Data JPA動(dòng)態(tài)條件查詢的寫法
本篇文章主要介紹了Spring Data JPA動(dòng)態(tài)條件查詢的寫法 ,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-06-06mybatis-plus中l(wèi)ambdaQuery()與lambdaUpdate()比較常見的使用方法總結(jié)
mybatis-plus是在mybatis的基礎(chǔ)上做增強(qiáng)不做改變,簡化了CRUD操作,下面這篇文章主要給大家介紹了關(guān)于mybatis-plus中l(wèi)ambdaQuery()與lambdaUpdate()比較常見的使用方法,需要的朋友可以參考下2022-09-09Java封裝數(shù)組之動(dòng)態(tài)數(shù)組實(shí)現(xiàn)方法詳解
這篇文章主要介紹了Java封裝數(shù)組之動(dòng)態(tài)數(shù)組實(shí)現(xiàn)方法,結(jié)合實(shí)例形式詳細(xì)分析了java動(dòng)態(tài)數(shù)組的實(shí)現(xiàn)原理、操作步驟與相關(guān)注意事項(xiàng),需要的朋友可以參考下2020-03-03springcloud項(xiàng)目快速開始起始模板的實(shí)現(xiàn)
本文主要介紹了springcloud項(xiàng)目快速開始起始模板思路的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12自帶IDEA插件的阿里開源診斷神器Arthas線上項(xiàng)目BUG調(diào)試
這篇文章主要為大家介紹了自帶IDEA插件阿里開源診斷神器Arthas線上項(xiàng)目BUG調(diào)試,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06SpringMVC自定義攔截器實(shí)現(xiàn)過程詳解
這篇文章主要介紹了SpringMVC自定義攔截器實(shí)現(xiàn)過程詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05java基礎(chǔ)篇之Date類型最常用的時(shí)間計(jì)算(相當(dāng)全面)
這篇文章主要給大家介紹了關(guān)于java基礎(chǔ)篇之Date類型最常用的時(shí)間計(jì)算的相關(guān)資料,Java中的Date類是用來表示日期和時(shí)間的類,它提供了一些常用的方法來處理日期和時(shí)間的操作,需要的朋友可以參考下2023-12-12