ThreadLocal原理介紹以及內(nèi)存泄漏分析

ThreadLocal簡單介紹

ThreadLocal同ReentrantLock,CyclicBarrier等都屬于并發(fā)工具類蜈出,他們都是為了解決多線程數(shù)據(jù)一致性問題而出現(xiàn)的。與ReentrantLock不同的是编检,ThreadLocal采取的是一種以空間換時間的策略甸饱。
舉個簡單的例子,假設(shè)現(xiàn)在有100個人填寫信息表境蜕,可是只有一支筆,為了防止哄搶凌停,以ReentrantLock為代表的鎖所使用的思路是粱年,通過控制人員使用筆的順序,來達到防止哄搶的目的罚拟。而ThreadLocal采取的思想則是給每個人發(fā)一只筆台诗,這樣大家只使用自己手頭里的筆,也不就不存在競爭問題了赐俗。
正如ThreadLocal其名拉队,ThreadLocal所擁有的變量是線程私有的,既然多個線程同時訪問一個共享變量會造成線程安全問題阻逮,那么我為什么不給每一個線程分配一個變量粱快,這樣就避免了多線程之間的同步,沒有了同步叔扼,代碼的運行所需要的時間就會減少事哭。雖然ThreadLocal的使用可以減少時間上的開銷,可是我們也很容易發(fā)現(xiàn)其會增大內(nèi)存空間上的開銷瓜富。

ThradLocal使用場景

SimpleDateFormat作為一個格式化日期的常用類鳍咱,卻存在著線程不安全的問題,運行下面的代碼

public static void main(String[] args)
{
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    ExecutorService es = Executors.newFixedThreadPool(100);
    for (int i = 0; i < 1000; i++)
    {
        es.execute(()->{
            try
            {
                Date date = sdf.parse("2019-11-13 08:23:" + new Random().nextInt(60));
            } catch (ParseException e)
            {
                e.printStackTrace();
            }
        });
    }
    es.shutdown();
}

會報出如下異常

java.lang.NumberFormatException: For input string: ""
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)

究其原因与柑,在于SimpleDateFormat類內(nèi)部有一個Calendar對象引用,它用來儲存和這個SimpleDateFormat相關(guān)的日期信息流炕,而這個對象又是線程共享的且沒有做任何同步處理澎现。

有了上面的分析,要解決這些異常也就不是什么難事了每辟。大家首先想到的可能是加鎖剑辫,即用synchronize或ReeentrantLock把調(diào)用parse方法那一部分包起來,但是這種方法在多線程競爭激烈的時候會帶來效率問題渠欺,代碼這里我就不寫了妹蔽。除了加鎖,還有一種更好的方法挠将,那便是使用ThreadLocal胳岂。

public static void main(String[] args)
{
   ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>();
   ExecutorService es = Executors.newFixedThreadPool(100);
   for (int i = 0; i < 1000; i++)
   {
       es.execute(()->{
           try
           {
               if(threadLocal.get() == null)
                   threadLocal.set(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
               Date date = threadLocal.get().parse("2019-11-13 08:23:" + new Random().nextInt(60));
           } catch (ParseException e)
           {
               e.printStackTrace();
           }
       });
   }
   es.shutdown();
}

ThreadLocal 可以確保每個線程都可以得到單獨的一個 SimpleDateFormat 的對象,那么自然也就不存在競爭問題了舔稀。


除了常用的SimpleDateFormat乳丰,我們還可以在Spring框架中找到ThreadLocal的身影。

@RestController
public class TestController
{
    @Autowired
    private HttpServletRequest request;
    
    @GetMapping("/hello")
    public String hello()
    {
        String token = request.getHeader("token");
        return token;
    }
}

對于上面的代碼内贮,細心的人可能會問产园,直接把request當(dāng)作一個成員變量注入,這樣所有請求將共享一個request對象夜郁,程序肯定會亂套啊什燕。但是當(dāng)我們運行上述代碼的時候,我們會發(fā)現(xiàn)程序并沒有什么問題竞端,這時為什么呢屎即。原因很簡單,spring是一個非常成熟的框架事富,當(dāng)我們要注入一個HttpServletRequest對象作為一個成員變量時技俐,它會以ThreadLocal的形式進行注入,這樣每個請求的request對象都是不同的统台。

ThreadLocal原理分析

看了上面的介紹雕擂,或許有人不禁要問,ThreadLocal這么強大饺谬,那它是怎么實現(xiàn)的呢。其實ThreadLocal與JUC中其他類的最大不同點是谣拣,ThreadLocal本身不存儲數(shù)據(jù)募寨,它更像一個工具類,負責(zé)變量的維護與獲取森缠,就像java.utilCollections類拔鹰,它本身并不存儲任何數(shù)據(jù)結(jié)構(gòu),但是可以完成許多數(shù)據(jù)結(jié)構(gòu)的操作贵涵。當(dāng)我們對ThreadLocal對象進行set操作時列肢,ThreadLocal并沒有把那些對象保存在自己這里恰画,而是保存在了調(diào)用該方法的Thread對象里

Thread類

Thread類內(nèi)部有許多成員變量瓷马,其中182行聲明的ThreadLocal.ThreadLocalMap對象就是用來保存Threadlocal執(zhí)行set方法時的對象
這樣拴还,當(dāng)調(diào)用get方法時,ThreadLocal會去調(diào)用該方法的Thread對象里去取之前set的value并返回欧聘∑郑看上去一切事那么的完美,可是當(dāng)一個線程有多個ThreadLocal對象來進行g(shù)et操作時怀骤,我們要怎么才能獲取到該ThreadLocal對應(yīng)的值呢费封?這還不簡單嘛,直接用一個map取維護ThreadLocal和value的映射不就行了蒋伦。對弓摘,java里就是這么做的,只不過這個map跟我們平常所見的HashMap痕届、TreeMap不太一樣韧献,是一個被稱為ThreadLocalMap的Map,這個類被定義為ThreadLocal的一個內(nèi)部靜態(tài)類爷抓,我們可以把它當(dāng)成一個HashMap來看待(如果仔細閱讀其源碼我們會發(fā)現(xiàn)其處理Hash沖突所采用的是線性探測法)

其三者UML關(guān)系如圖所示势决,其中Entry對象代表了ThreadLocalMap里的一個鍵值對。


ThreadLocal的get方法源碼如下

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();
    }

首先第二行獲取執(zhí)行該方法的當(dāng)前線程蓝撇,然后第三行調(diào)用getMap方法來獲取該線程對應(yīng)的ThreadLocalMap果复,其方法聲明如下

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

獲取到了Map之后首先進行判空處理,我們知道一個Map實際上是有許多Entry聚合而成的渤昌,而這些Entry保存的是所有的鍵值對(鍵為ThreadLocal虽抄,值為指定的泛型)信息。我們現(xiàn)在已經(jīng)獲取到了Map和鍵(當(dāng)前ThreadLocal)独柑,我們要獲取對應(yīng)的值迈窟,需要先去在該Map中根據(jù)該鍵去查找對應(yīng)的鍵值對,然后從這個鍵值對里獲取value忌栅。而map.getEntry(this)所做的就是去從這個當(dāng)前線程對應(yīng)的Map中去查找鍵位該ThreadLocal的鍵值對车酣。

ThreadLocal內(nèi)存泄漏問題

我們前面說過,ThreadLocalMap雖然可以當(dāng)作一個Map來使用索绪,但是其和一般的Map還是有一定的差別的湖员。在這里最重要的一點就是其鍵值對對象Entry的聲明

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

我們發(fā)現(xiàn)其繼承了一個WeakReference的對象。那么這個WeakReference對象是個什么鬼呢瑞驱,要講這個就必須要牽涉到j(luò)vm的垃圾回收了∧锼ぃ現(xiàn)代jvm采用的垃圾回收方法一般都是可達性分析,而一個對象是否可達則取決于是否存在一條從GCRoot到當(dāng)前對象的引用鏈唤反。java虛擬機規(guī)范里規(guī)定了四種引用類型凳寺,分別是強引用鸭津,軟引用,弱引用和虛引用肠缨。其中弱引用也就是WeakReference逆趋,每次垃圾回收時,如果發(fā)現(xiàn)有弱引用對象怜瞒,就將其回收父泳。
我們看到Entry繼承自WeakReference,并指定泛型為ThreadLocal吴汪,在構(gòu)造函數(shù)時調(diào)用了super(k);惠窄,這表明只要這個ThreadLocal失去了其他的強引用,該Entry就會被回收漾橙。

如圖杆融,此時Entry對象不會被回收,雖然ThreadLocal對象和Entry之間是弱引用霜运,但ThreadLocal引用和ThreadLocal是強引用脾歇。當(dāng)代碼執(zhí)行出ThreadLocal的作用域時,在棧上的ThreadLocal引用會被清除淘捡,此時在堆上的ThreadLocal對象只有一個Entry對象的引用藕各,由于此引用是弱引用,所以在下一次垃圾回收來臨時焦除,該ThreadLocal對象會被垃圾回收器回收激况。我們在不難發(fā)現(xiàn),ThreadLocal的Entry之所以設(shè)計成一個弱引用的對象膘魄,就是為了防止ThreadLocal對象內(nèi)存泄露乌逐。雖然解決了ThreadLocal對象的內(nèi)存泄漏,但是會產(chǎn)生一個新的問題创葡,那就是value對象的內(nèi)存泄露浙踢。當(dāng)ThreadLocal被回收后,ThreadLocalMap中就會出現(xiàn)key為null的Entry灿渴,就沒有辦法訪問這些key為null的Entry的value洛波,如果當(dāng)前線程再遲遲不結(jié)束的話,這些key為null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠無法回收骚露,造成內(nèi)存泄漏

其實蹬挤,ThreadLocalMap的設(shè)計中已經(jīng)考慮到這種情況,也加上了一些防護措施:在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap里所有key為null的value荸百。

但是這些被動的預(yù)防措施并不能保證不會內(nèi)存泄漏:

  • 使用static的ThreadLocal闻伶,延長了ThreadLocal的生命周期滨攻,可能導(dǎo)致的內(nèi)存泄漏
  • 分配使用了ThreadLocal又不再調(diào)用get(),set(),remove()方法够话,那么就會導(dǎo)致內(nèi)存泄漏蓝翰。

綜合上面的分析,我們可以理解ThreadLocal內(nèi)存泄漏的前因后果女嘲,那么怎么避免內(nèi)存泄漏呢畜份?

  • 每次使用完ThreadLocal,都調(diào)用它的remove()方法欣尼,清除數(shù)據(jù)爆雹。
  • 在使用線程池的情況下,沒有及時清理ThreadLocal愕鼓,不僅是內(nèi)存泄漏的問題钙态,更嚴重的是可能導(dǎo)致業(yè)務(wù)邏輯出現(xiàn)問題。所以菇晃,使用ThreadLocal就跟加鎖完要解鎖一樣册倒,用完就清理。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末磺送,一起剝皮案震驚了整個濱河市驻子,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌估灿,老刑警劉巖崇呵,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異馅袁,居然都是意外死亡域慷,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進店門司顿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芒粹,“玉大人,你說我怎么就攤上這事大溜』幔” “怎么了?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵钦奋,是天一觀的道長座云。 經(jīng)常有香客問我,道長付材,這世上最難降的妖魔是什么朦拖? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮厌衔,結(jié)果婚禮上璧帝,老公的妹妹穿的比我還像新娘。我一直安慰自己富寿,他們只是感情好睬隶,可當(dāng)我...
    茶點故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布锣夹。 她就那樣靜靜地躺著,像睡著了一般苏潜。 火紅的嫁衣襯著肌膚如雪银萍。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天恤左,我揣著相機與錄音贴唇,去河邊找鬼。 笑死飞袋,一個胖子當(dāng)著我的面吹牛戳气,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播巧鸭,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼物咳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蹄皱?” 一聲冷哼從身側(cè)響起览闰,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎巷折,沒想到半個月后压鉴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡锻拘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年油吭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片署拟。...
    茶點故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡婉宰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出推穷,到底是詐尸還是另有隱情心包,我是刑警寧澤,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布馒铃,位于F島的核電站蟹腾,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏区宇。R本人自食惡果不足惜娃殖,卻給世界環(huán)境...
    茶點故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望议谷。 院中可真熱鬧炉爆,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至衩辟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間波附,已是汗流浹背艺晴。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留掸屡,地道東北人封寞。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像仅财,于是被迫代替她去往敵國和親狈究。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,689評論 2 354