Java Thread Local 線程本地變量

更多 Java 并發(fā)編程方面的文章蚯妇,請參見文集《Java 并發(fā)編程》


Thread Local 線程本地變量

在每個線程中都創(chuàng)建一個變量的副本省容,線程內(nèi)共享封锉,線程間互斥铆遭。

ThreadLocal 是一個為線程提供線程局部變量的工具類磕瓷。為線程提供一個線程私有的變量副本盒齿,這樣多個線程都可以隨意更改自己線程局部的變量,不會影響到其他線程困食。
不過需要注意的是边翁,ThreadLocal 提供的只是一個淺拷貝,如果變量是一個引用類型硕盹,那么就要考慮它內(nèi)部的狀態(tài)是否會被改變符匾,想要解決這個問題可以通過重寫 ThreadLocal 的 initialValue() 函數(shù)來自己實現(xiàn)深拷貝,建議在使用 ThreadLocal 時一開始就重寫該函數(shù)瘩例。

ThreadLocal 與像 synchronized 這樣的鎖機制是不同的:

  • 鎖更強調(diào)的是如何同步多個線程去正確地共享一個變量啊胶,ThreadLocal 則是為了解決同一個變量如何不被多個線程共享
  • 從性能開銷的角度上來講,如果鎖機制是用時間換空間的話垛贤,那么 ThreadLocal 就是用空間換時間

ThreadLocal 中含有一個叫做 ThreadLocalMap 的內(nèi)部類焰坪,該類為一個采用線性探測法實現(xiàn)的 HashMap。
它的 key 為 ThreadLocal 對象而且還使用了 WeakReference聘惦,ThreadLocalMap 正是用來存儲變量副本的琳彩。

static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

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

    /**
     * The initial capacity -- MUST be a power of two.
     */
    private static final int INITIAL_CAPACITY = 16;

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

ThreadLocal 中只含有三個成員變量,這三個變量都是與 ThreadLocalMap 的 hash 策略相關的部凑。

public class ThreadLocal<T> {
    /**
     * ThreadLocals rely on per-thread linear-probe hash maps attached
     * to each thread (Thread.threadLocals and
     * inheritableThreadLocals).  The ThreadLocal objects act as keys,
     * searched via threadLocalHashCode.  This is a custom hash code
     * (useful only within ThreadLocalMaps) that eliminates collisions
     * in the common case where consecutively constructed ThreadLocals
     * are used by the same threads, while remaining well-behaved in
     * less common cases.
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

唯一的實例變量 threadLocalHashCode 是用來進行尋址的 hashcode,它由函數(shù) nextHashCode() 生成碧浊,該函數(shù)簡單地通過一個增量 HASH_INCREMENT 來生成 hashcode涂邀。至于為什么這個增量為 0x61c88647,主要是因為 ThreadLocalMap 的初始大小為16箱锐,每次擴容都會為原來的2倍比勉,這樣它的容量永遠為2的n次方,該增量選為 0x61c88647 也是為了盡可能均勻地分布驹止,減少碰撞沖突浩聋。

獲取當前線程中該變量的值 - get()

要獲得當前線程私有的變量副本需要調(diào)用 get() 函數(shù)。首先臊恋,它會調(diào)用 getMap() 函數(shù)去獲得當前線程的ThreadLocalMap衣洁,這個函數(shù)需要接收當前線程的實例作為參數(shù)。如果得到的 ThreadLocalMap 為 null抖仅,那么就去調(diào)用 setInitialValue() 函數(shù)來進行初始化坊夫,如果不為 null砖第,就通過 map 來獲得變量副本并返回。

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

/**
 * Variant of set() to establish initialValue. Used instead
 * of set() in case user has overridden the set() method.
 *
 * @return the initial value
 */
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

設置當前線程中該變量的值 - set()

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

移除當前線程中該變量的值 - remove()

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

getMap() 函數(shù)與 createMap() 函數(shù)的實現(xiàn)也十分簡單环凿,但是通過觀察這兩個函數(shù)可以發(fā)現(xiàn)一個秘密:ThreadLocalMap 是存放在 Thread 中的梧兼。

/**
 * Get the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param  t the current thread
 * @return the map
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

/**
 * Create the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param t the current thread
 * @param firstValue value for the initial entry of the map
 */
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

Thread 類中包括:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

仔細想想其實就能夠理解這種設計的思想。有一種普遍的方法是通過一個全局的線程安全的 Map 來存儲各個線程的變量副本智听,但是這種做法已經(jīng)完全違背了 ThreadLocal 的本意羽杰,設計 ThreadLocal 的初衷就是為了避免多個線程去并發(fā)訪問同一個對象,盡管它是線程安全的到推。
而在每個 Thread 中存放與它關聯(lián)的 ThreadLocalMap 是完全符合 ThreadLocal 的思想的考赛,當想要對線程局部變量進行操作時,只需要把 Thread 作為 key 來獲得 Thread 中的 ThreadLocalMap 即可环肘。這種設計相比采用一個全局 Map 的方法會多占用很多內(nèi)存空間欲虚,但也因此不需要額外的采取鎖等線程同步方法而節(jié)省了時間上的消耗。

使用示例

提供的方法:

  • T initialValue():設置初始值
  • public T get():獲取當前線程中該變量的值
  • public void set(T value):設置當前線程中該變量的值
  • public void remove():移除當前線程中該變量的值

示例:
我們希望每一個線程維護一個 Unique ID悔雹。

public class ThreadLocal_Test {
    private static final AtomicInteger nextId = new AtomicInteger(0);

    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return nextId.getAndIncrement();
        }
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new MyThread().start();
        }
    }

    static class MyThread extends Thread {
        public void run() {
            System.out.println(Thread.currentThread().getName() + " Unique ID: " + threadLocal.get());
        }
    }
}

輸出如下:

Thread-0 Unique ID: 0
Thread-1 Unique ID: 1
Thread-2 Unique ID: 2
Thread-3 Unique ID: 3
Thread-4 Unique ID: 4

ThreadLocal 中的內(nèi)存泄漏

如果 ThreadLocal 被設置為 null 后复哆,而且沒有任何強引用指向它,根據(jù)垃圾回收的可達性分析算法腌零,ThreadLocal 將會被回收梯找。這樣一來,ThreadLocalMap 中就會含有 key 為 null 的 Entry益涧,而且ThreadLocalMap 是在 Thread 中的锈锤,只要線程遲遲不結(jié)束,這些無法訪問到的 value 會形成內(nèi)存泄漏闲询。為了解決這個問題久免,ThreadLocalMap 中的 getEntry()、set() 和 remove() 函數(shù)都會清理 key 為 null 的 Entry扭弧。

在上文中我們發(fā)現(xiàn)了 ThreadLocalMap 的 key 是一個弱引用阎姥,那么為什么使用弱引用呢?使用強引用key與弱引用key的差別如下:

  • 強引用 key:ThreadLocal 被設置為 null鸽捻,由于 ThreadLocalMap 持有 ThreadLocal 的強引用呼巴,如果不手動刪除,那么 ThreadLocal 將不會回收御蒲,產(chǎn)生內(nèi)存泄漏衣赶。
  • 弱引用 key:ThreadLocal 被設置為 null,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用厚满,即便不手動刪除府瞄,ThreadLocal 仍會被回收,ThreadLocalMap 在之后調(diào)用 set()碘箍、getEntry() 和 remove() 函數(shù)時會清除所有 key 為 null 的 Entry摘能。

但要注意的是续崖,ThreadLocalMap僅僅含有這些被動措施來補救內(nèi)存泄漏問題。如果你在之后沒有調(diào)用ThreadLocalMap 的 set()团搞、getEntry() 和 remove() 函數(shù)的話严望,那么仍然會存在內(nèi)存泄漏問題。
在使用線程池的情況下逻恐,如果不及時進行清理像吻,內(nèi)存泄漏問題事小,甚至還會產(chǎn)生程序邏輯上的問題复隆。所以拨匆,為了安全地使用 ThreadLocal,必須要像每次使用完鎖就解鎖一樣挽拂,在每次使用完 ThreadLocal 后都要調(diào)用 remove() 來清理無用的Entry惭每。


引用:
聊一聊 Spring 中的線程安全性

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市亏栈,隨后出現(xiàn)的幾起案子台腥,更是在濱河造成了極大的恐慌,老刑警劉巖绒北,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件黎侈,死亡現(xiàn)場離奇詭異,居然都是意外死亡闷游,警方通過查閱死者的電腦和手機峻汉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脐往,“玉大人休吠,你說我怎么就攤上這事∫挡荆” “怎么了瘤礁?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長辖源。 經(jīng)常有香客問我,道長希太,這世上最難降的妖魔是什么克饶? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮誊辉,結(jié)果婚禮上矾湃,老公的妹妹穿的比我還像新娘。我一直安慰自己堕澄,他們只是感情好邀跃,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布霉咨。 她就那樣靜靜地躺著,像睡著了一般拍屑。 火紅的嫁衣襯著肌膚如雪途戒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天僵驰,我揣著相機與錄音喷斋,去河邊找鬼。 笑死蒜茴,一個胖子當著我的面吹牛星爪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播粉私,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼顽腾,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了诺核?” 一聲冷哼從身側(cè)響起抄肖,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎猪瞬,沒想到半個月后憎瘸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡陈瘦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年幌甘,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片痊项。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡锅风,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鞍泉,到底是詐尸還是另有隱情皱埠,我是刑警寧澤,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布咖驮,位于F島的核電站边器,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏托修。R本人自食惡果不足惜忘巧,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望睦刃。 院中可真熱鬧砚嘴,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至工育,卻和暖如春虾宇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背翅娶。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工文留, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人竭沫。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓燥翅,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蜕提。 傳聞我的和親對象是個殘疾皇子森书,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354

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

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn)谎势,斷路器凛膏,智...
    卡卡羅2017閱讀 134,656評論 18 139
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法脏榆,內(nèi)部類的語法猖毫,繼承相關的語法,異常的語法须喂,線程的語...
    子非魚_t_閱讀 31,631評論 18 399
  • 文章來源:http://www.54tianzhisheng.cn/2017/06/04/Java-Thread/...
    beneke閱讀 1,483評論 0 1
  • 本文主要講了java中多線程的使用方法吁断、線程同步、線程數(shù)據(jù)傳遞坞生、線程狀態(tài)及相應的一些線程函數(shù)用法仔役、概述等。 首先講...
    李欣陽閱讀 2,454評論 1 15
  • 6月--畢業(yè) 在學校上學時是己,多么希望早點畢業(yè)又兵,擺脫學校無用的課程和考試。畢業(yè)晚會最后一杯酒碰撞之后卒废,我知道沛厨,我終于...
    o翻滾的牛寶寶o閱讀 383評論 1 0