ThreadLocal是什么
早在JDK 1.2的版本中就提供java.lang.ThreadLocal殃饿,ThreadLocal為解決多線程程序的并發(fā)問(wèn)題提供了一種新的思路川陆。使用這個(gè)工具類可以很簡(jiǎn)潔地編寫出優(yōu)美的多線程程序铐尚。
Thread-local,很多地方叫做線程本地變量矛市,也有些地方叫做線程本地存儲(chǔ),其實(shí)意思差不多诲祸∽抢簦可能很多朋友都知道ThreadLocal為變量在每個(gè)線程中都創(chuàng)建了一個(gè)副本,那么每個(gè)線程可以訪問(wèn)自己內(nèi)部的副本變量救氯。
下面來(lái)看一個(gè)簡(jiǎn)單的示例:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ParseDate implements Runnable{
int i = 0;
public ParseDate(int i) {
this.i = i;
}
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public void run() {
try {
Date date = sdf.parse("2018-05-20 12:00:"+i%60);
System.out.println(i+":1"+date);
} catch (ParseException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//用線程池創(chuàng)建線程找田,
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
es.execute(new ParseDate(i));
}
}
}
運(yùn)行代碼后會(huì)出現(xiàn)這種錯(cuò)誤:
造成這樣錯(cuò)誤的原因是在多線程中使用simpleDateFormat.parse()方法并不是線程安全的,因此着憨,正在線程池中共享這個(gè)對(duì)象必然會(huì)導(dǎo)致報(bào)錯(cuò)墩衙。
一種可行的解決方案是在simpleDateFormat.parse()前后加鎖,這個(gè)也是我們一般的處理思路甲抖。但這里我們不這么做 漆改,我們使用ThreadLocal為每一個(gè)線程都產(chǎn)生simpleDateFormat對(duì)象實(shí)例:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ParseDate2 implements Runnable{
int i = 0;
public ParseDate2(int i) {
this.i = i;
}
private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>();
@Override
public void run() {
try {
if (threadLocal.get() == null) {
threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
Date date = threadLocal.get().parse("2018-05-20 12:00:"+i%60);
System.out.println(i+":1"+date);
} catch (ParseException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//用線程池創(chuàng)建線程,
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
es.execute(new ParseDate2(i));
}
}
}
注意這一段 if (threadLocal.get() == null)准谚,如果當(dāng)前線程不持有SimpleDateFormat對(duì)象實(shí)例挫剑。那么就新建一個(gè)并把它設(shè)置到當(dāng)前線程中,如果已經(jīng)持有柱衔,則 直接使用樊破。
從這里可以看到愉棱,為每一個(gè)線程都分配一個(gè)對(duì)象的工作并不是由ThreadLocal來(lái)完成的,而是需要在應(yīng)用層面保證的捶码,ThreadLoacl只是起到簡(jiǎn)單容器的作用羽氮。如果在應(yīng)用上為每一個(gè)線程分配了相同的對(duì)象實(shí)例,那么ThreadLocal也不能保證線程安全惫恼。
ThreadLoacl的實(shí)現(xiàn)原理
ThreadLocal是如何保證這些對(duì)象只能被當(dāng)前線程所訪問(wèn)呢?那我們下面來(lái)看一下具體ThreadLocal是如何實(shí)現(xiàn)的澳盐。
我們需要關(guān)注的祈纯,自然是ThreadLocal的set()方法和get()方法。先看一下set()方法:
在set時(shí)叼耙,首先通過(guò)Thread.currentThread()獲得當(dāng)前線程對(duì)象腕窥,然后通過(guò)getMap()拿到線程的ThreadLocalMap,并將值設(shè)置ThreadLocalMap中筛婉。ThreadLocalMap是Thread的內(nèi)部成員簇爆。
而設(shè)置到ThreadLocal中的數(shù)據(jù),也正是寫入threadLocals這個(gè)Map中爽撒。其中入蛆,key為ThreadLocal當(dāng)前對(duì)象,value就是我們需要的值硕勿。而threadLocals本身保存了當(dāng)前自己所在線程的所有“局部變量”哨毁,也就是ThreadLocal變量的集合。
在進(jìn)行變更get()操作時(shí)源武,自然是將這個(gè)map中的數(shù)據(jù)拿出來(lái):
首先扼褪,get()方法也是先獲取當(dāng)前線程的ThreadLocalMap對(duì)象。然后通過(guò)將自己作為key獲取vaule粱栖。
ThreadLocal的問(wèn)題
在了解ThreadLocal的內(nèi)部后话浇,我們自然會(huì)引出一個(gè)問(wèn)題,那就是這些變量是維護(hù)在Thread類的內(nèi)部闹究,這也意味著只要線程不退出幔崖,對(duì)象的引用就會(huì)一直存在。
當(dāng)線程退出時(shí)跋核,Thread類會(huì)進(jìn)行一些清理工作岖瑰,其中包括清理ThreadLocalMap。我們看一下Thread類的exit()方法砂代。
exit()方法在線程退出前蹋订,有系統(tǒng)回調(diào),進(jìn)行資源清理刻伊。
因此如果我們使用線程池露戒,那就意味著當(dāng)前線程未必退出(比如固定大小的線程池椒功,線程總是存在)。如果這樣智什,將一些比較大的對(duì)象設(shè)置到ThreadLocal中(它實(shí)際保存在線程持有的threadLocals這個(gè)Map中动漾,可能會(huì)使系統(tǒng)出現(xiàn)內(nèi)存泄漏(你設(shè)置了對(duì)象到ThreadLocal中,但是不清理它荠锭,在你是用幾次后旱眯,這個(gè)對(duì)象不再有用了,但是它卻無(wú)法被回收)证九。
此時(shí)删豺,如果你希望及時(shí)回收對(duì)象,最好使用ThreadLocal.remove()方法將這個(gè)變量移除愧怜。就像我們習(xí)慣性的關(guān)閉數(shù)據(jù)庫(kù)連接一樣呀页。如果你確實(shí)不需要這個(gè)對(duì)象了,那么就應(yīng)該告訴虛擬機(jī)拥坛,請(qǐng)把它回收掉蓬蝶,防止內(nèi)存泄漏。
另外一種有趣的情況是JDK有可能允許你像釋放普通變量一樣釋放ThreadLocal猜惋。比如丸氛,我們有時(shí)候?yàn)榱思铀倮厥眨瑫?huì)特意寫object =null之類的代碼惨奕,如果這么做雪位,obj所指向的對(duì)象就更容易被垃圾回收器發(fā)現(xiàn),從而加速垃圾回收梨撞。
同理雹洗,對(duì)于ThreadLocal的變量,我們也可以手動(dòng)將其設(shè)置為null卧波,比如threadLocal =null时肿。那么這個(gè)ThreadLocal對(duì)應(yīng)的所有線程的局部變量都有可能被回收。