Java并發(fā)編程之ThreadLocal原理

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ò)誤:


image.png

造成這樣錯(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()方法:


image.png

在set時(shí)叼耙,首先通過(guò)Thread.currentThread()獲得當(dāng)前線程對(duì)象腕窥,然后通過(guò)getMap()拿到線程的ThreadLocalMap,并將值設(shè)置ThreadLocalMap中筛婉。ThreadLocalMap是Thread的內(nèi)部成員簇爆。


image.png

而設(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):


image.png

首先扼褪,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()方法砂代。


image.png

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)存泄漏。


image.png

另外一種有趣的情況是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)的所有線程的局部變量都有可能被回收。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末港粱,一起剝皮案震驚了整個(gè)濱河市螃成,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌查坪,老刑警劉巖寸宏,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異偿曙,居然都是意外死亡氮凝,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門望忆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)罩阵,“玉大人竿秆,你說(shuō)我怎么就攤上這事「灞冢” “怎么了幽钢?”我有些...
    開封第一講書人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)傅是。 經(jīng)常有香客問(wèn)我匪燕,道長(zhǎng),這世上最難降的妖魔是什么喧笔? 我笑而不...
    開封第一講書人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任谎懦,我火速辦了婚禮,結(jié)果婚禮上溃斋,老公的妹妹穿的比我還像新娘。我一直安慰自己吸申,他們只是感情好梗劫,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著截碴,像睡著了一般梳侨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上日丹,一...
    開封第一講書人閱讀 51,718評(píng)論 1 305
  • 那天走哺,我揣著相機(jī)與錄音,去河邊找鬼哲虾。 笑死丙躏,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的束凑。 我是一名探鬼主播晒旅,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼汪诉!你這毒婦竟也來(lái)了废恋?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤扒寄,失蹤者是張志新(化名)和其女友劉穎鱼鼓,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體该编,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡迄本,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了上渴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片岸梨。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡喜颁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出曹阔,到底是詐尸還是另有隱情半开,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布赃份,位于F島的核電站寂拆,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏抓韩。R本人自食惡果不足惜纠永,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望谒拴。 院中可真熱鬧尝江,春花似錦、人聲如沸英上。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)苍日。三九已至惭聂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間相恃,已是汗流浹背辜纲。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拦耐,地道東北人耕腾。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像揩魂,于是被迫代替她去往敵國(guó)和親幽邓。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355