??ThreadLocal 初衷是在線程并發(fā)時(shí)犹芹,解決變量共享問(wèn)題顷帖,但由于過(guò)度設(shè)計(jì)臀脏,比如弱引用和哈希碰撞,導(dǎo)致理解難度大墓怀、使用成本高,反而成為故障高發(fā)點(diǎn)卫键,容易出現(xiàn)內(nèi)存泄漏傀履、臟數(shù)據(jù)、共享對(duì)象更新等問(wèn)題莉炉。
1. 引用類型
??對(duì)象在堆上創(chuàng)建之后所持有的引用其實(shí)是一種變量類型钓账,引用之間可以通過(guò)賦值構(gòu)成一條引用鏈。從GCRoots 開始遍歷呢袱,判斷引用是否可達(dá)官扣。引用的可達(dá)性是判斷能否被垃圾回收的基本條件。JVM 會(huì)據(jù)此自動(dòng)管理內(nèi)存的分配與回收羞福,不需要開發(fā)工程師干預(yù)惕蹄。但在某些場(chǎng)景下,即使引用可達(dá)治专,也希望能夠根據(jù)語(yǔ)義的強(qiáng)弱進(jìn)行有選擇的回收卖陵,以保證系統(tǒng)的正常運(yùn)行。根據(jù)引用類型語(yǔ)義的強(qiáng)弱來(lái)決定垃圾回收的階段张峰,我們可以把引用分為強(qiáng)引用泪蔫、軟引用、弱引用和虛引用四類喘批。后三類引用撩荣,本質(zhì)上是可以讓開發(fā)工程師通過(guò)代碼方式來(lái)決定對(duì)象的垃圾回收時(shí)機(jī)。我們先簡(jiǎn)要了解一下這四類引用饶深。
- 強(qiáng)引用餐曹,即 Strong Reference,最為常見敌厘。
如 Object object = new Object(); 這樣的變量聲明和定義就會(huì)產(chǎn)生對(duì)該對(duì)象的強(qiáng)引用台猴。只要對(duì)象有強(qiáng)引用指向,并且 GC Roots可達(dá)俱两,那么 Java 內(nèi)存回收時(shí)饱狂,即使瀕臨內(nèi)存耗盡,也不會(huì)回收該對(duì)象宪彩。 - 軟引用休讳,即 Sof Reference
引用力度弱于“強(qiáng)引用”,是用在非必需對(duì)象的場(chǎng)景在即將 OOM 之前尿孔,垃圾回收器會(huì)把這些軟引用指向的對(duì)象加入回收范圍衍腥,以獲得更多的內(nèi)存空間磺樱,讓程序能夠繼續(xù)健康運(yùn)行。主要用來(lái)緩存服務(wù)器中間計(jì)算結(jié)果及不需要實(shí)時(shí)保存的用戶行為等婆咸。 - 弱引用,即 Weak Reference
引用強(qiáng)度較前兩者更弱芜辕,也是用來(lái)描述非必需對(duì)象的尚骄。如果弱引用指向的對(duì)象只存在弱引用這一條線路,則在下一次YGC 時(shí)會(huì)被回收侵续。由于 YGC 時(shí)間的不確定性倔丈,弱引用何時(shí)被回收也具有不確定性。弱引用主要用于指向某個(gè)易消失的對(duì)象状蜗,在強(qiáng)引用斷開后需五,此引用不會(huì)劫持對(duì)象。調(diào)用 WeakReference.get()可能返回null轧坎,要注意空指針異常宏邮。 - 虛引用,即 Phantom Reference
是極弱的一種引用關(guān)系缸血,定義完成后蜜氨,就無(wú)法通過(guò)該引用獲取指向的對(duì)象。為一個(gè)對(duì)象設(shè)置虛引用的唯一目的就是希望能在這個(gè)對(duì)象被回收時(shí)收到一個(gè)系統(tǒng)通知捎泻。虛引用必須與引用隊(duì)列聯(lián)合使用飒炎,當(dāng)垃圾回收時(shí),如果發(fā)現(xiàn)存在虛引用笆豁,就會(huì)在回收對(duì)象內(nèi)存前郎汪,把這個(gè)虛引用加入與之關(guān)聯(lián)的引用隊(duì)列中。
對(duì)象的引用類型如圖所示闯狱。
對(duì)象引用類型
??舉個(gè)具體例子煞赢,在房產(chǎn)交易市場(chǎng)中,某個(gè)賣家有一套房子扩氢,成功出售賣給某個(gè)買家后引用置為 null耕驰。這里有4個(gè)買家使用4種不同的引用關(guān)系指向這套房子。買家buyer1是強(qiáng)引用录豺,如果把 seller 引用賦值給它朦肘,則永久有效,系統(tǒng)不會(huì)因?yàn)?seller=null就觸發(fā)對(duì)這套房子的回收双饥,這是房屋交易市場(chǎng)最常見的交付方式媒抠。買家 buyer2 是軟引用,只要不產(chǎn)生OOM咏花,buyer2.get() 就可以獲取房子對(duì)象趴生,就像房子是租來(lái)的一樣阀趴。買家 buyer3 是弱引用,一旦過(guò)戶后苍匆,seller 置為 null刘急,buyer3 的房子持有時(shí)間估計(jì)只有幾秒鐘,賣家只是給買家做了一張假的房產(chǎn)證浸踩,買家高興了幾秒鐘后叔汁,發(fā)現(xiàn)房子已經(jīng)不是自己的了。buyer4 是虛引用检碗,定義完成后無(wú)法訪問(wèn)到房子對(duì)象据块,賣家只是虛構(gòu)了房源,是空手套白狼的詐騙術(shù)折剃。
??強(qiáng)引用是最常用的另假,而虛引用在業(yè)務(wù)中幾乎很難用到。本文重點(diǎn)介紹一下軟引用和弱引用怕犁。先來(lái)說(shuō)明一下軟引用的回收機(jī)制边篮。首先設(shè)置JVM參數(shù):-Xms20mXmx20m,即只有50MB 的堆內(nèi)存空間因苹。在下方的示例代碼中不斷地往集合里添加House 對(duì)象苟耻,而每個(gè)House 有2000個(gè)Door 成員變量,狹小的堆空間加上大對(duì)象的產(chǎn)生扶檐,就是為了盡快觸達(dá)內(nèi)存耗盡的臨界狀態(tài):
idea 修改vm參數(shù): Run > Edit Configurations>Appliaction 找到應(yīng)用類 在右側(cè) VM options: 填寫-Xms50m -Xmx50m
public class SoftReferenceHouse {
public static void main(String[] args) {
//List<House> houses = new ArrayList<House>(); 第1處
List<SoftReference> houses = new ArrayList<SoftReference>();
int i = 0;
while (true) {
// houses.add(new House()); 第2處
//劇情反轉(zhuǎn)注釋處
SoftReference<House> buyer2 = new SoftReference<House>(new House());
//劇情發(fā)展注釋處
houses.add(buyer2);
System.out.println("i=" + (++i));
}
}
}
class House {
private static final Integer DOOR_NUMBER = 2000;
public Door[] doors = new Door[DOOR_NUMBER];
class Door {
}
}
new House0是匿名對(duì)象凶杖,產(chǎn)生之后即賦值給軟引用。正常運(yùn)行一段時(shí)間后款筑,內(nèi)存到達(dá)耗盡的臨界狀態(tài)智蝠,House$Door超過(guò)24MB左右,內(nèi)存占比達(dá)到50%奈梳,(可通過(guò)visualvm 查看杈湾,jconsole也可)
軟引用的特性在數(shù)秒之后產(chǎn)生價(jià)值,House對(duì)象數(shù)從千數(shù)量級(jí)迅速降到百數(shù)量級(jí)內(nèi)存容量迅速被釋放出來(lái)攘须,保證了程序的正常運(yùn)行漆撞,
??軟引用 SoftReference 的父類 Reference 的屬性: private T referent,它指向 newHouse()對(duì)象于宙,而SoftReference 的 get()浮驳,也是調(diào)用了 super.get() 來(lái)訪問(wèn)父類這個(gè)私有屬性。大量的 House 在內(nèi)存即將耗盡前捞魁,成功地一次又一次被清理掉至会。對(duì)象 buyer2雖然是引用類型,但其本身還是占用一定內(nèi)存空間的谱俭,它是被集合 ArrayList強(qiáng)引用劫持的奉件。在不斷循環(huán)執(zhí)行 houses.add(),后在=566581時(shí)宵蛀,終于產(chǎn)生了 OOM。軟引用县貌、弱引用术陶、虛引用均存在帶有隊(duì)列的構(gòu)造方法:
??public SoftReference(T referent, ReferenceQueue<? super T> 9){...}
??可以在隊(duì)列中檢查哪個(gè)軟引用的對(duì)象被回收了,從而把失去 House 的軟引用對(duì)象清理掉煤痕。
??反轉(zhuǎn)一下劇情瞳别。在同一個(gè)類中,使用完全相同的運(yùn)行環(huán)境和內(nèi)存參數(shù)杭攻,把SoftReference<House>中被注釋掉的兩句代碼激活( 即示例代碼中的第1處和第2處)同時(shí)把在后邊標(biāo)記了“劇情反轉(zhuǎn)注釋處”的3 句代碼注釋掉,再次運(yùn)行疤坝。觀察一下兆解,在沒(méi)有軟引用的情況下,這個(gè)循環(huán)能夠撐多久?運(yùn)行得到的結(jié)果在i=2404 時(shí)跑揉,就產(chǎn)生 OOM 異常锅睛。這個(gè)示例簡(jiǎn)單地證明了軟引用在內(nèi)存緊張情況下的回收能力。軟引用一般用于在同一服務(wù)器內(nèi)緩存中間結(jié)果历谍。如果命中緩存现拒,則提取緩存結(jié)果,否則重新計(jì)算或獲取望侈。但是印蔬,軟引用肯定不是用來(lái)緩存高頻數(shù)據(jù)的,萬(wàn)一服務(wù)器重啟或者軟引用觸發(fā)大規(guī)耐蜒茫回收侥猬,所有的訪問(wèn)將直接指向數(shù)據(jù)庫(kù),導(dǎo)致數(shù)據(jù)庫(kù)的壓力時(shí)大時(shí)小捐韩,甚至崩潰退唠。
如果內(nèi)存沒(méi)有達(dá)到OOM,軟引用持有的對(duì)象會(huì)被回收嗎?下面用代碼來(lái)驗(yàn)證一下
public class SoftReferenceWhenIdle {
public static void main(String[] args) {
House seller = new House();
//(第1處)
SoftReference<House> buyer2 = new SoftReference<House>(seller);
seller = null;
while (true) {
// 下方兩句代碼建議 JVM 進(jìn)行垃圾回收
System.gc();
System.runFinalization();
if (buyer2.get() == null) {
System.out.println("house is null");
break;
} else {
System.out.println("still there.");
}
}
}
}
??System.gc()方法建議垃圾收集器盡快進(jìn)行垃圾收集,具體何時(shí)執(zhí)行仍由JVM來(lái)
判斷荤胁。System.runFinalization0方法的作用是強(qiáng)制調(diào)用已經(jīng)失去引用對(duì)象的finalize()瞧预。
??在代碼中同時(shí)調(diào)用這兩者,有利于更快地執(zhí)行垃圾回收仅政。在相同的運(yùn)行環(huán)境下垢油,一直輸出stillthere,說(shuō)明 buyer2一直持有new House()的有效引用已旧。如果在對(duì)方置頭null 時(shí)仍能自動(dòng)感知秸苗,并且主動(dòng)斷開引用指向的對(duì)象,這是哪種引用方式可以擔(dān)負(fù)的使命?答案是弱引用运褪。事實(shí)上惊楼,把示例代碼中第1處的兩個(gè)紅色SoftReference 修改為 WeakReference 即可實(shí)現(xiàn)回收玖瘸。出于對(duì) WeakReference 的尊重,摒棄剛才催促垃圾回收的代碼檀咙,讓W(xué)eakReference 自然地被YGC 回收雅倒,使對(duì)象能夠存活更長(zhǎng)的時(shí)間我們可以在JVM啟動(dòng)參數(shù)加-XX:+PrintGCDetails(或高版本JDK使用-Xlog:gc)來(lái)觀察GC的觸發(fā)情況:
public class WeakReferenceWhenIdle {
public static void main(String[] args) {
House seller = new House();
WeakReference<House> buyer3 = new WeakReference<House>(seller);
long start = System.nanoTime();
int count = 0;
while (true) {
if (buyer3.get() == null) {
long duration = (System.nanoTime() - start) / (1000 * 1000);
System.out.println("house is null and exited time=" + duration + "ms");
} else {
System.out.println("still there. count =" + (count++));
}
}
}
}
執(zhí)行結(jié)果如下:
still there.count = 232639
[GC [PSYoungGen: 65536K->688K(76288K)] 65536K->696K(251392K), 0.0074719secs] [Times: user=0.01 sys-0.00弧可,real-0.01 secs]
still there. count = 232640
house is null and exited time = 1013ms
2. ThreadLocal價(jià)值
??我們從真人 CS 游戲說(shuō)起蔑匣。游戲開始時(shí),每個(gè)人能夠領(lǐng)到一把電子槍棕诵,槍把上有三個(gè)數(shù)字:子彈數(shù)裁良、殺敵數(shù)、自己的命數(shù)校套,為其設(shè)置的初始值分別為 1500价脾、0、10笛匙。假設(shè)戰(zhàn)場(chǎng)上的每個(gè)人都是一個(gè)線程侨把,那么這三個(gè)初始值寫在哪里呢?如果每個(gè)線程寫死這三個(gè)值,萬(wàn)一將初始子彈數(shù)統(tǒng)一改成 1000 發(fā)呢?如果共享妹孙,那么線程之間的并發(fā)修改會(huì)導(dǎo)致數(shù)據(jù)不準(zhǔn)確秋柄。能不能構(gòu)造這樣一個(gè)對(duì)象,將這個(gè)對(duì)象設(shè)置為共享變量蠢正,統(tǒng)一設(shè)置初始值骇笔,但是每個(gè)線程對(duì)這個(gè)值的修改都是互相獨(dú)立的。這個(gè)對(duì)象就是ThreadLocal机隙。注意不能將其翻譯為線程本地化或本地線程蜘拉,英語(yǔ)恰當(dāng)?shù)拿Q應(yīng)該叫作CopyValueIntoEveryThread。具體示例代碼如下:
public class CsGameByThreadLocal {
private static final Integer BULLET_NUMBER = 1500;
private static final Integer KILLED_ENEMIES = 0;
private static final Integer LIFE_VALUE = 10;
private static final Integer TOTAL_PLAYERS = 10;
// 隨機(jī)數(shù)用來(lái)展示每個(gè)對(duì)鼻的不同的數(shù)據(jù)(第1處)
private static final ThreadLocalRandom RANDOM = ThreadLocalRandom.current();
//初始化子彈數(shù)
private static final ThreadLocal<Integer> BULLET_NUMBER_THREADLOCAL = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return BULLET_NUMBER;
}
};
//初始化殺敵數(shù)
private static final ThreadLocal<Integer> KILLED_ENEMIES_THREADLOCAL = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return KILLED_ENEMIES;
}
};
//初始化自己的明叔
private static final ThreadLocal<Integer> LIFE_VALUE_THREADLOCAL = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return LIFE_VALUE;
}
};
//定義每位隊(duì)員
private static class Player extends Thread {
@Override
public void run() {
Integer bullets = BULLET_NUMBER_THREADLOCAL.get() - RANDOM.nextInt(BULLET_NUMBER);
Integer killEnemies = KILLED_ENEMIES_THREADLOCAL.get() + RANDOM.nextInt(TOTAL_PLAYERS / 2);
Integer lifeValue = LIFE_VALUE_THREADLOCAL.get() - RANDOM.nextInt(LIFE_VALUE);
System.out.println(getName() + ",BULLET NUMBER is " + bullets);
System.out.println(getName() + ",KILLED_ENEMIES is " + killEnemies);
System.out.println(getName() + ",LIEF_VALUES is " + lifeValue + "\n");
BULLET_NUMBER_THREADLOCAL.remove();
KILLED_ENEMIES_THREADLOCAL.remove();
LIFE_VALUE_THREADLOCAL.remove();
}
}
public static void main(String[] args) {
for (int i = 0; i < TOTAL_PLAYERS; i++) {
new Player().start();
}
}
}
??此示例中有鹿,沒(méi)有進(jìn)行 set 操作旭旭,那么初始值又是如何進(jìn)入每個(gè)線程成為獨(dú)立撓貝的呢?首先,雖然 ThreadLocal 在定義時(shí)覆寫了initialValue()方法葱跋,但并非是在BULLET_NUMBER_THREADLOCAL對(duì)象加載靜態(tài)變量的時(shí)候執(zhí)行的持寄,而是每個(gè)線程在ThreadLocal.get()的時(shí)候都會(huì)執(zhí)行到,其源碼如下
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();
}
??每個(gè)線程都有自己的ThreadLocalMap娱俺,如果map==null稍味,則直接執(zhí)行setInitialValue()。如果map 已經(jīng)創(chuàng)建荠卷,就表示Thread類的threadLocals屬性已經(jīng)初始化:如果e==null模庐,依然會(huì)執(zhí)行到 setInitialValue0。setInitialValue()的源碼如下
protected T initialValue() {
return null;
}
private T setInitialValue() {
// 這是一個(gè)保護(hù)方法油宜,CsGameByThreadLocal 中初始化ThreadLocal 對(duì)象時(shí)已覆寫
T value = initialValue();
Thread t = Thread.currentThread();
// getMap 的源碼就是提取線程對(duì)象t的ThreadLocalMap 屬性:t.threadLocals
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
??在 CsGameByThreadLocal 類的第 1處掂碱,使用了ThreadLocalRandom 生成單獨(dú)的Random 實(shí)例怜姿。此類在JDK7 中引入,它使得每個(gè)線程都可以有自己的隨機(jī)數(shù)生成器疼燥。我們要避免 Random實(shí)例被多線程使用沧卢,雖然共享該實(shí)例是線程安全的,但會(huì)因競(jìng)爭(zhēng)同一seed 而導(dǎo)致性能下降醉者。
??我們已經(jīng)知道了 ThreadLocal是每一個(gè)線程單獨(dú)持有的但狭。因?yàn)槊恳粋€(gè)線程都有獨(dú)立的變量副本,其他線程不能訪問(wèn)撬即,所以不存在線程安全問(wèn)題立磁,也不會(huì)影響程序的執(zhí)行性能。ThreadLocal 對(duì)象通常是由 private static 修飾的剥槐,因?yàn)槎夹枰獜?fù)制進(jìn)入本地線程息罗,所以非 static作用不大。需要注意的是,ThreadLocal無(wú)法解決共享對(duì)象的更新問(wèn)題,下面的代碼實(shí)例將證明這點(diǎn)才沧。因?yàn)?CsGameByThreadLocal中使用的是Integer 的不可變對(duì)象,所以可以使用相同的編碼方式來(lái)操作一下可變對(duì)象看看绍刮,示例源碼如下:
public class InitValueInThreadLocal {
private static final StringBuilder INIT_VALUE = new StringBuilder("init");
//覆寫ThreadLocal的initalValue温圆、返回StringBuilder靜態(tài)引用
private static final ThreadLocal<StringBuilder> builder = new ThreadLocal<StringBuilder>() {
@Override
protected StringBuilder initialValue() {
return INIT_VALUE;
}
};
private static class AppendStringThread extends Thread {
@Override
public void run() {
StringBuilder inThread = builder.get();
for (int i = 0; i < 10; i++) {
inThread.append("_" + i);
}
System.out.println(inThread.toString());
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new AppendStringThread().start();
}
TimeUnit.SECONDS.sleep(10);
}
}
??輸出的結(jié)果是亂序不可控的,所以使用某個(gè)引用來(lái)操作共享對(duì)象時(shí)孩革,依然需要進(jìn)行線程同步岁歉。
??ThreadLocal 有個(gè)靜態(tài)內(nèi)部類叫 ThreadLocalMap,它還有一個(gè)靜態(tài)內(nèi)部類叫 Entry膝蜈, 在 Thread 中的 ThreadLocalMap 屬性的賦值是在 ThreadLocal 類中的createMap() 中進(jìn)行的锅移。ThreadLocal 與 ThreadLocalMap 有三組對(duì)應(yīng)的方法: get()、set()和 remove0饱搏,在ThreadLocal中對(duì)它們只做校驗(yàn)和判斷非剃,最終的實(shí)現(xiàn)會(huì)落在ThreadLocalMap上。Entry繼承自 WeakReference,沒(méi)有方法推沸,只有一個(gè)value 成員變量它的key是ThreadLocal對(duì)象备绽。
- 1個(gè) Thread 有且僅有1個(gè) ThreadLocalMap 對(duì)象;
- 1個(gè) Entry 對(duì)象的 Key 弱引用指向1個(gè) ThreadLocal 對(duì)象;
- 1個(gè) ThreadLocalMap 對(duì)象存儲(chǔ)多個(gè)Entry 對(duì)象
- 1個(gè)ThreadLocal對(duì)象可以被多個(gè)線程所共享
- ThreadLocal對(duì)象不持有 Value,Value 由線程的 Entry 對(duì)象持有鬓催。
??最后肺素,SimpleDateFormat 是線程不安全的類,定義static 對(duì)象宇驾,會(huì)有數(shù)據(jù)同步風(fēng)險(xiǎn)倍靡。通過(guò)源碼可以看出,SimpleDateFormat 內(nèi)部有一個(gè) Calendar 對(duì)象课舍,在日期轉(zhuǎn)字符串或字符串轉(zhuǎn)日期的過(guò)程中塌西,多線程共享時(shí)有非常高的概率產(chǎn)生錯(cuò)誤他挎,推薦的方式之一就是使用 ThreadLocal,讓每個(gè)線程單獨(dú)擁有這個(gè)對(duì)象雨让。示例代碼如下:
private static final ThreadLocal<DateFormat> DATE_FORMAT_THREADLOCAL = new ThreadLocal<DateFormat>(){
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
3.ThreadLocal副作用
??為了使線程安全地共享某個(gè)變量雇盖,JDK 開出了 ThreadLocal 這劑藥方。但“是藥三分毒”栖忠,ThreadLocal有一定的副作用崔挖,所以需要仔細(xì)閱讀藥方說(shuō)明書,了解藥性和注意事項(xiàng)庵寞。ThreadLocal 的主要問(wèn)題是會(huì)產(chǎn)塵數(shù)據(jù)和內(nèi)存泄漏狸相。這兩個(gè)問(wèn)題通常是在線程池的線程中使用 ThreadLocal 引發(fā)的,因?yàn)榫€程范有線程復(fù)用和內(nèi)存常駐兩個(gè)特點(diǎn)捐川。
??1.臟數(shù)據(jù)
??線程復(fù)用會(huì)產(chǎn)生臟數(shù)據(jù)脓鹃。由于線程池會(huì)重用 Thread 對(duì)象,那么與 Thread 綁定的類的靜態(tài)屬性 ThreadLocal 變量也會(huì)被重用古沥。如果在實(shí)現(xiàn)的線程 run()方法體中不顯式地調(diào)用remove()清理與線程相關(guān)的 ThreadLocal信息瘸右,那么倘若下一個(gè)線程不調(diào)用set()設(shè)置初始值,就可能 get()到重用的線程信息岩齿,包括 ThreadLocal 所關(guān)聯(lián)的線程對(duì)象的 value 值太颤。
??臟數(shù)據(jù)問(wèn)題在實(shí)際故障中十分常見。比如盹沈,用戶 A 下單后沒(méi)有看到訂單記錄龄章,而用戶 B 卻看到了用戶A 的訂單記錄。通過(guò)排查發(fā)現(xiàn)是由于 session 優(yōu)化引發(fā)的乞封。在原來(lái)的請(qǐng)求過(guò)程中做裙,用戶每次請(qǐng)求 Server,都需要通過(guò) sessionId 去緩存里查詢用戶的session 信息肃晚,這樣做無(wú)疑增加了一次調(diào)用锚贱。因此,開發(fā)工程師決定采用某框架來(lái)緩存每個(gè)用戶對(duì)應(yīng)的 SecurityContext关串,它封裝了 session 相關(guān)信息惋鸥。優(yōu)化后雖然會(huì)為每個(gè)用戶新建一個(gè)session 相關(guān)的上下文,但是由于Threadlocal沒(méi)有在線程處理結(jié)束時(shí)及時(shí)進(jìn)行 remove()清理操作悍缠,在高并發(fā)場(chǎng)景下卦绣,線程池中的線程可能會(huì)讀取到上一個(gè)線程緩存的用戶信息。為了便于理解飞蚓,用一段簡(jiǎn)要代碼來(lái)模擬滤港,如下所示:
public class DirtyDataInThreadLocal {
public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
public static void main(String[] args) {
// 使用固定大小為1的線程池,說(shuō)明上一個(gè)的線程屬性會(huì)被下一個(gè)線程屬性復(fù)用
ExecutorService pool = Executors.newFixedThreadPool(1);
for (int i = 0; i < 2; i++) {
Mythead mythead = new Mythead();
pool.execute(mythead);
}
}
private static class Mythead extends Thread {
private static boolean flag = true;
@Override
public void run() {
if (flag) {
//第1個(gè)線程set后,并沒(méi)有進(jìn)行remove
//而第二個(gè)線程由于某種原因沒(méi)有進(jìn)行set 操作
threadLocal.set(getName() + ". session info.");
flag = false;
}
System.out.println(getName() + " 線程是 " + threadLocal.get());
}
}
}
執(zhí)行結(jié)果如下:
Thread-0 線程是 Thread-0. session info.
Thread-1 線程是 Thread-0. session info.
??2.內(nèi)存泄漏
??在源碼注釋中提示使用 static 關(guān)鍵字來(lái)修飾 ThreadLocal溅漾。在此場(chǎng)景下山叮,寄希望于ThreadLocal對(duì)象失去引用后,觸發(fā)弱引用機(jī)制來(lái)回收 Entry 的 Value就不現(xiàn)實(shí)了添履。在上例中屁倔,如果不進(jìn)行 remove()操作,那么這個(gè)線程執(zhí)行完成后暮胧,通過(guò)ThreadLocal對(duì)象持有的String對(duì)象是不會(huì)被釋放的锐借。
??以上兩個(gè)問(wèn)題的解決辦法很簡(jiǎn)單,就是在每次用完 ThreadLocal時(shí)往衷,必須要及時(shí)調(diào)用remove()方法清理钞翔。