ThreadLocal實(shí)現(xiàn)原理和應(yīng)用實(shí)例

java多線程編程中奇唤,線程安全是個(gè)很重要的概念。在保證線程安全的手段中匹摇,除了同步咬扇、鎖、不變對(duì)象外廊勃,還有一種很重要的方法便是線程封閉技術(shù)懈贺,就是將可變對(duì)象封閉在一個(gè)線程中,同時(shí)只能由一個(gè)線程訪問(wèn)該對(duì)象坡垫。實(shí)現(xiàn)線程封閉的一種常用方法便是使用ThreadLocal類梭灿,這個(gè)類能使線程中的某個(gè)值與保存的對(duì)象關(guān)聯(lián)起來(lái)。ThreadLocal為每個(gè)使用變量的線程都存有一份副本冰悠,因此get總是返回由當(dāng)前線程set的最新值堡妒。那么ThreadLocal如何實(shí)現(xiàn)變量的線程封閉的呢?下面我們通過(guò)對(duì)ThreadLocal的源碼分析來(lái)解讀其實(shí)現(xiàn)原理溉卓。

實(shí)現(xiàn)原理

類的關(guān)系和職責(zé)

ThreadLocal中定義了一個(gè)內(nèi)部靜態(tài)類ThreadLocalMap皮迟,該類是線程本地變量的容器——一個(gè)類似HashMap的容器。這兩個(gè)類再加上Thread類共同配合實(shí)現(xiàn)了線程封閉技術(shù)桑寨。首先伏尼,我們來(lái)看一下這幾類的關(guān)系圖

ThreadLocal類關(guān)系圖

從圖中我們可看出這幾個(gè)類有如下重要關(guān)系:

  1. Thread持有一個(gè)包級(jí)別的ThreadLocalMap成員變量threadLocals,兩者屬于聚合關(guān)系

  2. ThreadLocal通過(guò)Thread.currentThread()靜態(tài)方法獲得對(duì)當(dāng)前線程的引用依賴

  3. 由于ThreadLocal類與Thread類在同一個(gè)包下尉尾,因此ThreadLocal可以自由訪問(wèn)Thread.threadLocals變量爆阶,因此ThreadLocal類則負(fù)責(zé)創(chuàng)建和初始化threadLocals變量

  4. 我們自定義的應(yīng)用類ApplicationClass類只依賴ThreadLocal類,并訪問(wèn)該類的公有方法可以寫(xiě)入和讀取線程的本地變量

  5. ThreadLocalMap的key是ThreadLocal對(duì)象沙咏,value是ThreadLocal中存放的變量

圖中類之間的關(guān)系還是比較繞的扰她,但對(duì)于使用者來(lái)說(shuō)相對(duì)簡(jiǎn)單,我們只關(guān)心ThreadLocal類便可以了芭碍。最常用的ThreadLocal類方法public T get()public void set(T value)徒役,還有一個(gè)protected作用域的方法protected T initialValue()用于初始化變量值。這里我們會(huì)發(fā)現(xiàn)窖壕,ThreadLocal類似于Facade設(shè)計(jì)模式的門(mén)面類忧勿,對(duì)外提供簡(jiǎn)單的接口,對(duì)內(nèi)協(xié)調(diào)Thread和ThreadLocalMap之間的關(guān)系瞻讽。而真正實(shí)現(xiàn)需求核心邏輯的是類Thread和ThreadLocalMap鸳吸。

那么,我們可以定義這么一個(gè)需求:我們需要將一個(gè)或多個(gè)可變對(duì)象的封閉在一個(gè)線程中速勇,使得一個(gè)可變對(duì)象在不進(jìn)行同步的情況下晌砾,同時(shí)只能有一個(gè)線程可以訪問(wèn)該變量,從而實(shí)現(xiàn)線程安全烦磁。對(duì)于這個(gè)需求养匈,我們明確一下三個(gè)類的職責(zé)關(guān)系哼勇,可能更容易理解:

  1. Thread類是使線程封閉的主體,對(duì)象的訪問(wèn)權(quán)限就是封閉在當(dāng)前運(yùn)行的線程中呕乎,只能由當(dāng)前線程訪問(wèn)(這也是為什么會(huì)由Thread類以聚合的關(guān)系持有ThreadLocalMap對(duì)象)

  2. 我們的需求是可以實(shí)現(xiàn)多個(gè)可變對(duì)象的線程封閉积担,因此,我們定義了一個(gè)map容器用于存儲(chǔ)多個(gè)可變對(duì)象猬仁,這個(gè)類便是ThreadLocalMap類帝璧,這個(gè)map的key是ThreadLocal對(duì)象,value是可變對(duì)象(也就是那個(gè)被封閉起來(lái)的寶寶)湿刽。

  3. ThreadLocal對(duì)外提供簡(jiǎn)單的方法的烁,同時(shí)由于ThreadLocal和可變對(duì)象是一對(duì)一的關(guān)系,正好可以作為T(mén)hreadLocalMap的key與可變對(duì)象做關(guān)聯(lián)诈闺。

經(jīng)過(guò)以上的分析渴庆,我們會(huì)發(fā)現(xiàn),這個(gè)設(shè)計(jì)非常巧妙买雾,既遵循了類職責(zé)單一的面向?qū)ο笤瓌t,又實(shí)現(xiàn)了我們的需求杨帽,同時(shí)對(duì)使用者非常友好漓穿。

ThreadLocalMap實(shí)現(xiàn)關(guān)鍵點(diǎn)

通過(guò)以上類關(guān)系和職責(zé)的分析,我們知道ThreadLocal的核心業(yè)務(wù)邏輯其實(shí)是在ThreadLocalMap中的注盈,那么晃危,接下來(lái)我們就重點(diǎn)關(guān)注一下ThreadLocalMap實(shí)現(xiàn)中的幾個(gè)關(guān)鍵點(diǎn)。

從代碼中我們發(fā)現(xiàn)老客,ThreadLocalMap其實(shí)就是一個(gè)簡(jiǎn)版的HashMap僚饭,在這個(gè)特定的map中的存儲(chǔ)的是一個(gè)Entry數(shù)組,而ThreadLocalMap中的Entry實(shí)現(xiàn)非常簡(jiǎn)單卻也很特別:

static class ThreadLocalMap {
    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
    */
    private Entry[] table;

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

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

這個(gè)Entry是一個(gè)弱引用胧砰,而ThreadLocalMap中的table數(shù)組變量只是持有ThreadLocal對(duì)象的虛引用鳍鸵。這里為什么要這么設(shè)計(jì)呢?我們不妨回憶一下幾種引用的特性尉间。

  1. 強(qiáng)引用(Strong Reference)就是我們最常見(jiàn)的普通對(duì)象引用偿乖,只要還有強(qiáng)引用指向這個(gè)對(duì)象,就表明對(duì)象還“活著”哲嘲,垃圾收集器不會(huì)碰這種對(duì)象贪薪。對(duì)于一個(gè)普通的對(duì)象,如果沒(méi)有其他的引用關(guān)系眠副,只要超過(guò)了引用的作用域或者顯式地將相應(yīng)(強(qiáng))引用賦值為 null画切,就是可以被垃圾收集的了,當(dāng)然具體回收時(shí)機(jī)還是要看垃圾收集策略囱怕。
  2. 軟引用(SoftReference)是一種相對(duì)強(qiáng)引用弱化一些的引用霍弹,可以讓對(duì)象豁免一些垃圾收集毫别,只有當(dāng) JVM 認(rèn)為內(nèi)存不足時(shí),才會(huì)去試圖回收軟引用指向的對(duì)象庞萍。JVM 會(huì)確保在拋出 OutOfMemoryError 之前拧烦,清理軟引用指向的對(duì)象。軟引用通常用來(lái)實(shí)現(xiàn)內(nèi)存敏感的緩存钝计,如果還有空閑內(nèi)存恋博,就可以暫時(shí)保留緩存,當(dāng)內(nèi)存不足時(shí)清理掉私恬,這樣就保證了使用緩存的同時(shí)债沮,不會(huì)耗盡內(nèi)存。
  3. 弱引用(WeakReference)并不能使對(duì)象被豁免垃圾回收本鸣,僅僅提供一種訪問(wèn)在弱引用狀態(tài)下對(duì)象的途徑疫衩。被弱引用關(guān)聯(lián)的對(duì)象,在垃圾回收時(shí)荣德,如果這個(gè)對(duì)象只被弱引用關(guān)聯(lián)(沒(méi)有任何強(qiáng)引用關(guān)聯(lián)他)闷煤,那么這個(gè)對(duì)象就會(huì)被回收。這就可以用來(lái)構(gòu)建一種沒(méi)有特定約束的關(guān)系涮瞻,比如鲤拿,維護(hù)一種非強(qiáng)制性的映射關(guān)系,如果試圖獲取時(shí)對(duì)象還在署咽,就使用它近顷,否則重新實(shí)例化。它同樣是很多緩存實(shí)現(xiàn)的選擇宁否。
  4. 虛引用(PhantomReference)不能通過(guò)它訪問(wèn)對(duì)象窒升。幻象引用僅僅是提供了一種確保對(duì)象被 finalize 以后慕匠,做某些事情的機(jī)制饱须,比如,通常用來(lái)做所謂的 Post-Mortem 清理機(jī)制台谊,也有人利用幻象引用監(jiān)控對(duì)象的創(chuàng)建和銷(xiāo)毀冤寿。

這里我們重點(diǎn)關(guān)注一下弱引用的特性:只有弱引用指向的對(duì)象是不影響垃圾收集器對(duì)其回收的。那么青伤,ThreadLocalMap中的Entry為什么定義為T(mén)hreadLocal的弱引用類呢督怜?這里我們不妨反向去思考,假設(shè)這里不用虛引用的實(shí)現(xiàn)狠角,而是類似HashMap中的強(qiáng)引用号杠,會(huì)有什么問(wèn)題嗎?我們知道除了我們自定義的應(yīng)用類持有ThreadLocal的強(qiáng)引用外,還存在一條線程到ThreadLocal的引用鏈條:Thread.threadLocals --> ThreadLocalMap.table --> ThreadLocal對(duì)象姨蟋。在很多應(yīng)用尤其是后端服務(wù)器應(yīng)用中屉凯,會(huì)創(chuàng)建很多線程,并且線程的生命的周期往往比很多對(duì)象生命周期要長(zhǎng)很多眼溶。在使用ThreadLocal的過(guò)程中悠砚,不在引用作用域范圍內(nèi)的ThreadLocal對(duì)象或手動(dòng)置空(threadLocal=null)的ThreadLocal對(duì)象,是可以被垃圾回收掉的堂飞。而如果在線程到ThreadLocal的引用鏈中是強(qiáng)引用灌旧,那么這個(gè)ThreadLocal對(duì)象的生命周期將伴隨著Thread對(duì)象的存活而一直存在。這其實(shí)是不利于垃圾回收和內(nèi)存利用的绰筛,相當(dāng)于白白浪費(fèi)了ThreadLocal對(duì)象占用的內(nèi)存空間枢泰。因此,為了不影響垃圾收集器對(duì)ThreadLocal變量的回收铝噩,這里Entry的實(shí)現(xiàn)用了WeakReference衡蚂。

從上面代碼分析中,我們也會(huì)了解到為了盡快實(shí)現(xiàn)垃圾收集器對(duì)ThreadLocal以及相關(guān)聯(lián)的對(duì)象的回收骏庸,對(duì)于我們不再使用的ThreadLocal變量毛甲,最好將相關(guān)的強(qiáng)引用置空,以避免內(nèi)存溢出具被。代碼如下示例所示:

// threadLocal是事先定義好的ThreadLocal變量
threadLocal.remove();
threadLocal = null;

應(yīng)用實(shí)例

在一些框架或基礎(chǔ)類庫(kù)中玻募,ThreadLocal的使用還是比較廣泛的。下面就舉幾個(gè)常見(jiàn)的例子:

concurent包中的應(yīng)用

在讀寫(xiě)鎖java.util.concurrent.locks.ReentrantReadWriteLock實(shí)現(xiàn)中硬猫,ThreadLocal用于記錄當(dāng)前線程持有讀鎖的次數(shù):

/**
 * ThreadLocal subclass. Easiest to explicitly define for sake
 * of deserialization mechanics.
*/
static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}
/**
 * The number of reentrant read locks held by current thread.
 * Initialized only in constructor and readObject.
 * Removed whenever a thread's read hold count drops to 0.
*/
private transient ThreadLocalHoldCounter readHolds;

spring中的事務(wù)管理

spring中的事務(wù)管理补箍,也大量使用了ThreadLocal改执,代碼如下:

// spring-core中定義的NamedThreadLocal
public class NamedThreadLocal<T> extends ThreadLocal<T> {
    private final String name;

    public NamedThreadLocal(String name) {
        Assert.hasText(name, "Name must not be empty");
        this.name = name;
    }

    public String toString() {
        return this.name;
    }
}
// spring-tx包中定義的事務(wù)管理器
public abstract class TransactionSynchronizationManager {

    private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);

    private static final ThreadLocal<Map<Object, Object>> resources =
            new NamedThreadLocal<>("Transactional resources");

    private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
            new NamedThreadLocal<>("Transaction synchronizations");

    private static final ThreadLocal<String> currentTransactionName =
            new NamedThreadLocal<>("Current transaction name");

    private static final ThreadLocal<Boolean> currentTransactionReadOnly =
            new NamedThreadLocal<>("Current transaction read-only status");

    private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
            new NamedThreadLocal<>("Current transaction isolation level");

    private static final ThreadLocal<Boolean> actualTransactionActive =
            new NamedThreadLocal<>("Actual transaction active");
    // ...
}

在spring的事務(wù)管理框架通過(guò)將事務(wù)上下文信息保存在ThreadLocal對(duì)象中啸蜜,實(shí)現(xiàn)了事務(wù)上線文與執(zhí)行線程的關(guān)聯(lián)關(guān)系,從而使事務(wù)管理與數(shù)據(jù)訪問(wèn)服務(wù)解耦辈挂,同時(shí)也保證了多線程環(huán)境下connection的線程安全問(wèn)題衬横。

避免濫用

不過(guò),開(kāi)發(fā)人員也經(jīng)常會(huì)濫用ThreadLocal终蒂,例如將所有全局變量都作為T(mén)hreadLocal對(duì)象蜂林,或者作為一種“隱藏”的傳參手段。但是ThreadLocal也有全局變量的缺點(diǎn)拇泣,它降低了代碼的可重用性噪叙,并在類之間引入隱含的耦合性,因此霉翔,在使用時(shí)需要謹(jǐn)慎睁蕾,必要的情況下再使用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市子眶,隨后出現(xiàn)的幾起案子瀑凝,更是在濱河造成了極大的恐慌,老刑警劉巖臭杰,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粤咪,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡渴杆,警方通過(guò)查閱死者的電腦和手機(jī)寥枝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)将塑,“玉大人脉顿,你說(shuō)我怎么就攤上這事〉懔龋” “怎么了艾疟?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)敢辩。 經(jīng)常有香客問(wèn)我蔽莱,道長(zhǎng),這世上最難降的妖魔是什么戚长? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任盗冷,我火速辦了婚禮,結(jié)果婚禮上同廉,老公的妹妹穿的比我還像新娘仪糖。我一直安慰自己,他們只是感情好迫肖,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布锅劝。 她就那樣靜靜地躺著,像睡著了一般蟆湖。 火紅的嫁衣襯著肌膚如雪故爵。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天隅津,我揣著相機(jī)與錄音诬垂,去河邊找鬼。 笑死伦仍,一個(gè)胖子當(dāng)著我的面吹牛结窘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播充蓝,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼隧枫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起悠垛,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤线定,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后确买,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體斤讥,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年湾趾,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了芭商。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡搀缠,死狀恐怖铛楣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情艺普,我是刑警寧澤簸州,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站歧譬,受9級(jí)特大地震影響岸浑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瑰步,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一矢洲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧缩焦,春花似錦读虏、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至呻拌,卻和暖如春葱轩,著一層夾襖步出監(jiān)牢的瞬間睦焕,已是汗流浹背藐握。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留垃喊,地道東北人猾普。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像本谜,于是被迫代替她去往敵國(guó)和親初家。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容