ThreadLocal大家都不陌生匪燕,字面意思是線程本地副本摸吠,可在多線程環(huán)境下,為每個(gè)線程創(chuàng)建獨(dú)立的副本保證線程安全,在需要線程隔離的場(chǎng)合應(yīng)用很廣泛畜疾,但是關(guān)于ThreadLocal赴邻,總是有兩個(gè)疑惑:
- 聽(tīng)說(shuō)ThreadLocal中有有使用弱引用,為什么要用弱引用啡捶?用弱引用姥敛,發(fā)生一次gc后,set進(jìn)去的值再get就是null了嗎瞎暑?
- 聽(tīng)說(shuō)ThreadLocal可能引起內(nèi)存泄露彤敛?啥場(chǎng)景會(huì)內(nèi)存泄露?為何使用了弱引用依然可能發(fā)生內(nèi)存泄露了赌?怎么避免墨榄?
首先先來(lái)一段代碼,看下最基本的使用:我們聲明兩個(gè)線程勿她,將線程的名字通過(guò)ThreadLocal保存袄秩,然后再通過(guò)ThreadLocal取出,看一下每個(gè)線程獲取到的線程名字
public class TestThreadLocal {
final static ThreadLocal<String> LOCAL = new ThreadLocal();
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 線程1
executorService.execute(() -> {
// 存值
LOCAL.set(Thread.currentThread().getName());
// 獲取值
System.out.println(Thread.currentThread().getName() + "-->" +LOCAL.get());
});
// 線程2
executorService.execute(() -> {
LOCAL.set(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName() + "-->" +LOCAL.get());
});
executorService.shutdown();
}
}
運(yùn)行結(jié)果
pool-1-thread-1-->pool-1-thread-1
pool-1-thread-2-->pool-1-thread-2
結(jié)果沒(méi)有什么懸念逢并,每一個(gè)線程都獲取到了與自己相對(duì)于的名字之剧。
現(xiàn)在我們就點(diǎn)源碼,看下它內(nèi)部是怎么存儲(chǔ)和獲取數(shù)據(jù)的(源碼基于jdk1.8砍聊,不同版本的jdk實(shí)現(xiàn)方式可能稍有不同)
首先看下ThreadLocal的set()
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
源碼短短幾行背稼,首先獲取當(dāng)前線程,然后調(diào)用getMap()玻蝌,返回一個(gè)ThreadLocalMap蟹肘,暫且不管這個(gè)ThreadLocalMap是什么,通過(guò)名字我們簡(jiǎn)單猜測(cè)灶伊,就是一個(gè)map疆前,我們繼續(xù)往下看,如果map不為空直接保存數(shù)據(jù)聘萨,map為空則創(chuàng)建然后再保存數(shù)據(jù)竹椒,而保存數(shù)據(jù)的方法,key傳入的this米辐,也就是當(dāng)前的ThreadLocal對(duì)象胸完,value是我們要保存的值
(所以注意了,我們不能說(shuō)ThreadLocal能保存線程獨(dú)享的變量翘贮,而是保存數(shù)據(jù)的鑰匙赊窥,通過(guò)它操作ThreadLocalMap)。
我們一直在說(shuō)ThreadLocalMap狸页,現(xiàn)在回過(guò)頭來(lái)锨能,看看ThreadLocalMap是什么扯再,怎么來(lái)的吧。首先看看它的由來(lái):ThreadLocalMap map = getMap(t)
址遇,點(diǎn)進(jìn)去熄阻,很簡(jiǎn)單,獲取了當(dāng)前線程的成員變量:ThreadLocal.ThreadLocalMap threadLocals
倔约,我們可以理解為秃殉,每個(gè)線程在實(shí)例化的時(shí)候,都會(huì)創(chuàng)建一個(gè)ThreadLocalMap實(shí)例浸剩,保存線程獨(dú)享的數(shù)據(jù)钾军。
然后我們?cè)诳纯碩hreadLocalMap吧,該類的源碼在ThreadLocal類中绢要,是一個(gè)靜態(tài)的class吏恭,簡(jiǎn)單看一下ThreadLocalMap的實(shí)現(xiàn)
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í)現(xiàn),不要戀戰(zhàn)袖扛,我們不難看出2點(diǎn):
- 雖然它的名字叫Map砸泛,但并沒(méi)有實(shí)現(xiàn)java.util.Map接口十籍,而是自己?jiǎn)为?dú)實(shí)現(xiàn)的蛆封。
- 同大多數(shù)的Map的實(shí)現(xiàn)類似,其內(nèi)部也是維護(hù)了一個(gè)Entry存儲(chǔ)數(shù)據(jù)勾栗,Entry里有key和value惨篱,其中的value在Entry里聲明,但是key卻并沒(méi)有直接在Entry里聲明围俘,而是繼承WeakReference砸讳,是一個(gè)弱引用,在WeakReference的父類Reference里界牡,聲明了
T referent
簿寂,即為該map的key
好的,還記得剛剛我們看的ThreadLocal的set()嗎宿亡,先獲取ThreadLocalMap實(shí)例常遂,然后調(diào)用ThreadLocalMap的set(),我們來(lái)看一下ThreadLocal的set()吧挽荠,我們依舊不要戀戰(zhàn)克胳,沒(méi)必要一行一行的讀,我們大致看一下就好了
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
這就是個(gè)簡(jiǎn)易版的Map的put存放數(shù)據(jù)的方法圈匆,相信大家都知道HashMap的實(shí)現(xiàn)漠另,對(duì)此應(yīng)該很清楚,大體上就是根據(jù)當(dāng)前哈希桶容量和key的哈希值跃赚,計(jì)算一個(gè)存放角標(biāo)笆搓,將存值的時(shí)候,沒(méi)有當(dāng)前key,直接新增一個(gè)Entry(上文說(shuō)過(guò)满败,Entry的key是弱引用哦)窘奏,有當(dāng)前key,替換掉其value葫录。
但說(shuō)明一點(diǎn)着裹,與HashMap這種哈希鏈表存儲(chǔ)不同的是,在尋址沖突時(shí)米同,ThreadLocalMap并沒(méi)有使用鏈表或紅黑樹(shù)等方式鏈地址來(lái)解決骇扇,而是當(dāng)前地址不可用,就在當(dāng)前map的數(shù)據(jù)數(shù)組中繼續(xù)查找下一個(gè)可用的地址面粮,有興趣的可以仔細(xì)看下少孝。
兜了一圈,一句話總結(jié)這個(gè)ThreadLocal的set(T value)熬苍,就是在當(dāng)前線程的ThreadLocalMap里存放了數(shù)據(jù)稍走,key是使用弱引用的ThreadLocal,value就是我們set進(jìn)去的value
ThreadLocal的獲取值等其他方法就不做過(guò)多分析了柴底,下面重點(diǎn)分析下開(kāi)始時(shí)拋出的問(wèn)題一:關(guān)于弱引用的問(wèn)題婿脸。
弱引用,在經(jīng)歷一次gc后柄驻,不管當(dāng)前內(nèi)存是否足夠狐树,都會(huì)被清除,我們把開(kāi)始的代碼修改一下鸿脓,在通過(guò)ThreadLocal保存數(shù)據(jù)后抑钟,停頓一秒,然后在main線程中觸發(fā)一次gc野哭,然后在在線程中通過(guò)ThreadLocal獲取數(shù)據(jù)在塔,看會(huì)不會(huì)被清除。為了確認(rèn)到底有沒(méi)有發(fā)生gc拨黔,在啟動(dòng)時(shí)我們加入?yún)?shù)
-XX:+PrintGCDetails
public class TestThreadLocal {
static ThreadLocal<String> LOCAL = new ThreadLocal();
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(3);
executorService.execute(() -> {
// 存值
LOCAL.set(Thread.currentThread().getName());
try {
// 停頓一秒蛔溃,以便先在gc,再get
Thread.sleep(1000l);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 獲取值
System.out.println(Thread.currentThread().getName() + "-->" +LOCAL.get());
});
// 線程二
executorService.execute(() -> {
LOCAL.set(Thread.currentThread().getName());
try {
Thread.sleep(1000l);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->" +LOCAL.get());
});
// 主線程中觸發(fā)gc
System.gc();
executorService.shutdown();
}
}
結(jié)果如下蓉驹,如舊成功獲取了數(shù)據(jù)
[GC (System.gc()) [PSYoungGen: 5243K->784K(76288K)] 5243K->792K(251392K), 0.0028957 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 784K->0K(76288K)] [ParOldGen: 8K->597K(175104K)] 792K->597K(251392K), [Metaspace: 3724K->3724K(1056768K)], 0.0119867 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
pool-1-thread-1-->pool-1-thread-1
pool-1-thread-2-->pool-1-thread-2
可見(jiàn)ThreadLocal的使用沒(méi)有受到gc的影響城榛,原因何在?
我們先分析一下里面的引用鏈态兴,其中實(shí)線為強(qiáng)引用狠持,虛線為弱引用
可見(jiàn),現(xiàn)在的ThreadLocal瞻润,是有兩條引用鏈的喘垂,一條是當(dāng)前線程中的甜刻,由線程指向ThreadLocalMap,通過(guò)Map指向Entry正勒,而Entry指向key得院;另一條引用鏈則是當(dāng)前執(zhí)行的測(cè)試類的成員變量:TestThreadLocal#LOCAL,且為強(qiáng)引用章贞,所以目前來(lái)說(shuō)并不會(huì)受到gc影響祥绞。
我們?cè)賮?lái)看下問(wèn)題二,內(nèi)存泄露的問(wèn)題鸭限,還是來(lái)段代碼跑跑再說(shuō)蜕径,這段代碼,主要做的就是败京,分別通過(guò)new Thread()和線程池的方式開(kāi)100個(gè)線程兜喻,每個(gè)線程都向ThreadLocal存入1M大小的對(duì)象,為了盡快實(shí)驗(yàn)出效果赡麦,我們把最大堆內(nèi)存調(diào)小點(diǎn)
-Xmx50m -XX:+PrintGCDetails
public class TestThreadLocalLeak {
final static ThreadLocal<byte[]> LOCAL = new ThreadLocal();
final static int _1M = 1024 * 1024;
public static void main(String[] args) {
//testUseThread();
testUseThreadPool();
}
/**
* 使用線程
*/
private static void testUseThread() {
for (int i = 0; i < 100; i++) {
new Thread(() ->
LOCAL.set(new byte[_1M])
).start();
}
}
/**
* 使用線程池
*/
private static void testUseThreadPool() {
ExecutorService executorService = Executors.newFixedThreadPool(100);
for (int i = 0; i < 100; i++) {
executorService.execute(() ->
LOCAL.set(new byte[_1M])
);
}
executorService.shutdown();
}
}
使用線程打印結(jié)果(部分日志)
[GC (Allocation Failure) [PSYoungGen: 13819K->1712K(13824K)] 24099K->11992K(48128K), 0.0007287 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 12586K->1280K(14336K)] 22866K->12181K(48640K), 0.0008377 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 12257K->1120K(14336K)] 23158K->12021K(48640K), 0.0006637 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 12191K->1216K(14336K)] 23093K->12117K(48640K), 0.0010607 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
使用線程池打印結(jié)果(部分日志)
[Full GC (Ergonomics) java.lang.OutOfMemoryError: Java heap space
[PSYoungGen: 12800K->2080K(14848K)] [ParOldGen: 33327K->33322K(34304K)] 46127K->35402K(49152K), [Metaspace: 3770K->3770K(1056768K)], 0.0129146 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
當(dāng)調(diào)用testUseThread()時(shí)朴皆,系統(tǒng)在運(yùn)行時(shí)執(zhí)行了大量YGC,但始終穩(wěn)定回收泛粹,最后正常執(zhí)行遂铡,但是執(zhí)行testUseThreadPool()時(shí),經(jīng)歷的頻繁的Full GC戚扳,內(nèi)存卻沒(méi)有降下去忧便,最終發(fā)生了OOM族吻。
我們分析一下帽借,在使用new Thread()的時(shí)候,當(dāng)線程執(zhí)行完畢時(shí)超歌,隨著線程的終止砍艾,那個(gè)這個(gè)Thread對(duì)象的生命周期也就結(jié)束了,此時(shí)該線程下的成員變量巍举,ThreadLocalMap是GC Root不可達(dá)的脆荷,同理,下面的Entry懊悯、里面的key蜓谋、value都會(huì)在下一次gc時(shí)被回收;而使用線程池后炭分,由于線程執(zhí)行完一個(gè)任務(wù)后桃焕,不會(huì)被回收,而是被放回線程池以便執(zhí)行后續(xù)任務(wù)捧毛,自然其成員變量ThreadLocalMap不會(huì)被回收观堂,最終引起內(nèi)存泄露直至OOM让网。至于怎么避免出現(xiàn)內(nèi)存泄露,就是在使用線程完成任務(wù)后师痕,如果保存在ThreadLocalMap中的數(shù)據(jù)不必留給之后的任務(wù)重復(fù)使用溃睹,就要及時(shí)調(diào)用ThreadLocal的remove(),這個(gè)方法會(huì)把ThreadLocalMap中的相關(guān)key和value分別置為null胰坟,就能在下次GC時(shí)回收了因篇。
最后,我們回過(guò)頭來(lái)笔横,再看下問(wèn)題一中的一個(gè)疑問(wèn):ThreadLocalMap的Entry的key惜犀,為什么使用弱引用?還記得我們說(shuō)狠裹,ThreadLocal是有兩條引用鏈嗎虽界?那么我們斷掉強(qiáng)引用,看看弱引用的表現(xiàn)吧涛菠。
這次來(lái)段代碼莉御,我們自己debug一下
public class TestThreadLocalLeak {
static ThreadLocal LOCAL = new ThreadLocal();
public static void main(String[] args) {
LOCAL.set("測(cè)試ThreadLocalMap弱引用自動(dòng)回收");
Thread thread = Thread.currentThread();
LOCAL = null;
System.gc();
System.out.println("");
}
}
在gc前和gc后打斷點(diǎn),之前我們分析了俗冻,之所以ThreadLocal的數(shù)據(jù)不會(huì)被回收礁叔,是因?yàn)橛袃蓚€(gè)引用鏈指向ThreadLocal,一個(gè)是當(dāng)前線程的ThreadLocalMap迄薄,另一條就是當(dāng)前類中的成員變量LOCAL琅关,所以我們手動(dòng)把LOCAL置為null,再次調(diào)用System.gc()讥蔽,看一下弱引用是不是被回收了
System.gc()前
System.gc()后
可見(jiàn)涣易,執(zhí)行完gc后,確實(shí)回收了弱引用key冶伞,但是value并沒(méi)有被回收新症,原因當(dāng)然是他是強(qiáng)引用。
上面例子都是基于自己的理解自己寫的demo响禽,如果理解的不到位或錯(cuò)誤之處徒爹,歡迎大家不吝賜教,謝謝芋类!
2020-12-22更新
關(guān)于ThreadLocal使用的討論
看到有些編碼規(guī)范上隆嗅,對(duì)使用ThreadLocal有如下要求和建議:
(強(qiáng)制)在代碼邏輯中使用完ThreadLocal,都要調(diào)用remove方法侯繁,及時(shí)清理胖喳。
(推薦)盡量不要使用全局的ThreadLocal。
關(guān)于強(qiáng)制的要求的解讀為:目前我們的項(xiàng)目中使用的線程巫击,通常是對(duì)線程池化管理的(不管是我們自定義的線程池或是tomcat的線程池等)禀晓,核心線程數(shù)之內(nèi)的線程都是長(zhǎng)期駐留池內(nèi)的精续。如果不能及時(shí)調(diào)用remove,一方面可能造成數(shù)據(jù)泄露粹懒,另一方面有可能讓使用了上次未清除的值重付,導(dǎo)致嚴(yán)重的業(yè)務(wù)邏輯問(wèn)題。所以推薦在ThreadLocal使用前后都調(diào)用remove清理凫乖,同時(shí)針對(duì)異常情況也要在finally中清理确垫。
關(guān)于推薦不使用全局ThreadLocal,假設(shè)我們?nèi)质褂昧薚hreadLocal帽芽,那么這個(gè)引用可能保留給了多個(gè)業(yè)務(wù)使用删掀,當(dāng)有某業(yè)務(wù)線程修改了該ThreadLocal引用的實(shí)例后,會(huì)造成其他業(yè)務(wù)線程獲不到解決等不符合預(yù)期的問(wèn)題导街。