Java 之 ThreadLocal 詳解

1. 概念

ThreadLocal 用于提供線程局部變量挪丢,在多線程環(huán)境可以保證各個(gè)線程里的變量獨(dú)立于其它線程里的變量。也就是說 ThreadLocal 可以為每個(gè)線程創(chuàng)建一個(gè)【單獨(dú)的變量副本】,相當(dāng)于線程的 private static 類型變量。

ThreadLocal 的作用和同步機(jī)制有些相反:同步機(jī)制是為了保證多線程環(huán)境下數(shù)據(jù)的一致性;而 ThreadLocal 是保證了多線程環(huán)境下數(shù)據(jù)的獨(dú)立性。

2. 使用示例

public class ThreadLocalTest {
    private static String strLabel;
    private static ThreadLocal<String> threadLabel = new ThreadLocal<>();

    public static void main(String... args) {
        strLabel = "main";
        threadLabel.set("main");

        Thread thread = new Thread() {

            @Override
            public void run() {
                super.run();
                strLabel = "child";
                threadLabel.set("child");
            }

        };

        thread.start();
        try {
            // 保證線程執(zhí)行完畢
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("strLabel = " + strLabel);
        System.out.println("threadLabel = " + threadLabel.get());
    }
}

運(yùn)行結(jié)果:

strLabel = child
threadLabel = main

從運(yùn)行結(jié)果可以看出唧席,對于 ThreadLocal 類型的變量,在一個(gè)線程中設(shè)置值嘲驾,不影響其在其它線程中的值袱吆。也就是說 ThreadLocal 類型的變量的值在每個(gè)線程中是獨(dú)立的。

3. ThreadLocal 實(shí)現(xiàn)

ThreadLocal 是怎樣保證其值在各個(gè)線程中是獨(dú)立的呢距淫?下面分析下 ThreadLocal 的實(shí)現(xiàn)绞绒。

ThreadLocal 是構(gòu)造函數(shù)只是一個(gè)簡單的無參構(gòu)造函數(shù),并且沒有任何實(shí)現(xiàn)榕暇。

3.1 set(T value) 方法

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

set(T value) 方法中蓬衡,首先獲取當(dāng)前線程,然后在獲取到當(dāng)前線程的 ThreadLocalMap彤枢,如果 ThreadLocalMap 不為 null狰晚,則將 value 保存到 ThreadLocalMap 中,并用當(dāng)前 ThreadLocal 作為 key缴啡;否則創(chuàng)建一個(gè) ThreadLocalMap 并給到當(dāng)前線程壁晒,然后保存 value。

ThreadLocalMap 相當(dāng)于一個(gè) HashMap业栅,是真正保存值的地方秒咐。

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

同樣的,在 get() 方法中也會(huì)獲取到當(dāng)前線程的 ThreadLocalMap碘裕,如果 ThreadLocalMap 不為 null携取,則把獲取 key 為當(dāng)前 ThreadLocal 的值;否則調(diào)用 setInitialValue() 方法返回初始值帮孔,并保存到新創(chuàng)建的 ThreadLocalMap 中雷滋。

3.3 initialValue() 方法:

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;
}
...

initialValue() 是 ThreadLocal 的初始值,默認(rèn)返回 null,子類可以重寫改方法晤斩,用于設(shè)置 ThreadLocal 的初始值焕檬。

3.4 remove() 方法

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

ThreadLocal 還有一個(gè) remove() 方法,用來移除當(dāng)前 ThreadLocal 對應(yīng)的值澳泵。同樣也是同過當(dāng)前線程的 ThreadLocalMap 來移除相應(yīng)的值揩页。

3.5 當(dāng)前線程的 ThreadLocalMap

在 set,get烹俗,initialValue 和 remove 方法中都會(huì)獲取到當(dāng)前線程,然后通過當(dāng)前線程獲取到 ThreadLocalMap萍程,如果 ThreadLocalMap 為 null幢妄,則會(huì)創(chuàng)建一個(gè) ThreadLocalMap,并給到當(dāng)前線程茫负。

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

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
...

可以看到蕉鸳,每一個(gè)線程都會(huì)持有有一個(gè) ThreadLocalMap,用來維護(hù)線程本地的值:

public class Thread implements Runnable {
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ...
}

在使用 ThreadLocal 類型變量進(jìn)行相關(guān)操作時(shí)忍法,都會(huì)通過當(dāng)前線程獲取到 ThreadLocalMap 來完成操作潮尝。每個(gè)線程的 ThreadLocalMap 是屬于線程自己的,ThreadLocalMap 中維護(hù)的值也是屬于線程自己的饿序。這就保證了 ThreadLocal 類型的變量在每個(gè)線程中是獨(dú)立的勉失,在多線程環(huán)境下不會(huì)相互影響。

4. ThreadLocalMap

4.1 構(gòu)造方法

ThreadLocal 中當(dāng)前線程的 ThreadLocalMap 為 null 時(shí)會(huì)使用 ThreadLocalMap 的構(gòu)造方法新建一個(gè) ThreadLocalMap:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

構(gòu)造方法中會(huì)新建一個(gè)數(shù)組原探,并將將第一次需要保存的鍵值存儲(chǔ)到一個(gè)數(shù)組中乱凿,完成一些初始化工作。

4.2 存儲(chǔ)結(jié)構(gòu)

ThreadLocalMap 內(nèi)部維護(hù)了一個(gè)哈希表(數(shù)組)來存儲(chǔ)數(shù)據(jù)咽弦,并且定義了加載因子:

// 初始容量徒蟆,必須是 2 的冪
private static final int INITIAL_CAPACITY = 16;

// 存儲(chǔ)數(shù)據(jù)的哈希表
private Entry[] table;

// table 中已存儲(chǔ)的條目數(shù)
private int size = 0;

// 表示一個(gè)閾值,當(dāng) table 中存儲(chǔ)的對象達(dá)到該值時(shí)就會(huì)擴(kuò)容
private int threshold;

// 設(shè)置 threshold 的值
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

table 是一個(gè) Entry 類型的數(shù)組型型,Entry 是 ThreadLocalMap 的一個(gè)內(nèi)部類段审。

4.3 存儲(chǔ)對象 Entry

Entry 用于保存一個(gè)鍵值對闹蒜,其中 key 以弱引用的方式保存:

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

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

4.4 保存鍵值對

調(diào)用 set(ThreadLocal key, Object value) 方法將數(shù)據(jù)保存到哈希表中:

private void set(ThreadLocal key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    // 計(jì)算要存儲(chǔ)的索引位置
    int i = key.threadLocalHashCode & (len-1);

    // 循環(huán)判斷要存放的索引位置是否已經(jīng)存在 Entry寺枉,若存在,進(jìn)入循環(huán)體
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal k = e.get();

        // 若索引位置的 Entry 的 key 和要保存的 key 相等绷落,則更新該 Entry 的值
        if (k == key) {
            e.value = value;
            return;
        }

        // 若索引位置的 Entry 的 key 為 null(key 已經(jīng)被回收了)型凳,表示該位置的 Entry 已經(jīng)無效,用要保存的鍵值替換該位置上的 Entry
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 要存放的索引位置沒有 Entry嘱函,將當(dāng)前鍵值作為一個(gè) Entry 保存在該位置
    tab[i] = new Entry(key, value);
    // 增加 table 存儲(chǔ)的條目數(shù)
    int sz = ++size;
    // 清除一些無效的條目并判斷 table 中的條目數(shù)是否已經(jīng)超出閾值
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash(); // 調(diào)整 table 的容量甘畅,并重新擺放 table 中的 Entry
}

首先使用 key(當(dāng)前 ThreadLocal)的 threadLocalHashCode 來計(jì)算要存儲(chǔ)的索引位置 i。threadLocalHashCode 的值由 ThreadLocal 類管理,每創(chuàng)建一個(gè) ThreadLocal 對象都會(huì)自動(dòng)生成一個(gè)相應(yīng)的 threadLocalHashCode 值疏唾,其實(shí)現(xiàn)如下:

// ThreadLocal 對象的 HashCode
private final int threadLocalHashCode = nextHashCode();

// 使用 AtomicInteger 保證多線程環(huán)境下的同步
private static AtomicInteger nextHashCode =
    new AtomicInteger();

// 每次創(chuàng)建 ThreadLocal 對象是 HashCode 的增量
private static final int HASH_INCREMENT = 0x61c88647;

// 計(jì)算 ThreadLocal 對象的 HashCode
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

在保存數(shù)據(jù)時(shí)蓄氧,如果索引位置有 Entry,且該 Entry 的 key 為 null槐脏,那么就會(huì)執(zhí)行清除無效 Entry 的操作喉童,因?yàn)?Entry 的 key 使用的是弱引用的方式,key 如果被回收(即 key 為 null)顿天,這時(shí)就無法再訪問到 key 對應(yīng)的 value堂氯,需要把這樣的無效 Entry 清除掉來騰出空間。

在調(diào)整 table 容量時(shí)牌废,也會(huì)先清除無效對象咽白,然后再根據(jù)需要擴(kuò)容。

private void rehash() {
    // 先清除無效 Entry
    expungeStaleEntries();
    // 判斷當(dāng)前 table 中的條目數(shù)是否超出了閾值的 3/4
    if (size >= threshold - threshold / 4)
        resize();
}

清除無用對象和擴(kuò)容的方法這里就不再展開說明了鸟缕。

4.5 獲取 Entry 對象

取值是直接獲取到 Entry 對象晶框,使用 getEntry(ThreadLocal key) 方法:

private Entry getEntry(ThreadLocal key) {
    // 使用指定的 key 的 HashCode 計(jì)算索引位置
    int i = key.threadLocalHashCode & (table.length - 1);
    // 獲取當(dāng)前位置的 Entry
    Entry e = table[i];
    // 如果 Entry 不為 null 且 Entry 的 key 和 指定的 key 相等,則返回該 Entry
    // 否則調(diào)用 getEntryAfterMiss(ThreadLocal key, int i, Entry e) 方法
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

因?yàn)榭赡艽嬖诠_突懂从,key 對應(yīng)的 Entry 的存儲(chǔ)位置可能不在通過 key 計(jì)算出的索引位置上授段,也就是說索引位置上的 Entry 不一定是 key 對應(yīng)的 Entry。所以需要調(diào)用 getEntryAfterMiss(ThreadLocal key, int i, Entry e) 方法獲取番甩。

private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    // 索引位置上的 Entry 不為 null 進(jìn)入循環(huán)侵贵,為 null 則返回 null
    while (e != null) {
        ThreadLocal k = e.get();
        // 如果 Entry 的 key 和指定的 key 相等,則返回該 Entry
        if (k == key)
            return e;
        // 如果 Entry 的 key 為 null (key 已經(jīng)被回收了)缘薛,清除無效的 Entry
        // 否則獲取下一個(gè)位置的 Entry模燥,循環(huán)判斷
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

4.6 移除指定的 Entry

private void remove(ThreadLocal key) {
    Entry[] tab = table;
    int len = tab.length;
    // 使用指定的 key 的 HashCode 計(jì)算索引位置
    int i = key.threadLocalHashCode & (len-1);
    // 循環(huán)判斷索引位置的 Entry 是否為 null
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        // 若 Entry 的 key 和指定的 key 相等,執(zhí)行刪除操作
        if (e.get() == key) {
            // 清除 Entry 的 key 的引用
            e.clear();
            // 清除無效的 Entry
            expungeStaleEntry(i);
            return;
        }
    }
}

4.7 內(nèi)存泄漏

在 ThreadLocalMap 的 set()掩宜,get() 和 remove() 方法中蔫骂,都有清除無效 Entry 的操作,這樣做是為了降低內(nèi)存泄漏發(fā)生的可能牺汤。

Entry 中的 key 使用了弱引用的方式辽旋,這樣做是為了降低內(nèi)存泄漏發(fā)生的概率,但不能完全避免內(nèi)存泄漏檐迟。

這句話的意思好象是矛盾的补胚,下面來分析一下。

假設(shè) Entry 的 key 沒有使用弱引用的方式追迟,而是使用了強(qiáng)引用:由于 ThreadLocalMap 的生命周期和當(dāng)前線程一樣長溶其,那么當(dāng)引用 ThreadLocal 的對象被回收后,由于 ThreadLocalMap 還持有 ThreadLocal 和對應(yīng) value 的強(qiáng)引用敦间,ThreadLocal 和對應(yīng)的 value 是不會(huì)被回收的瓶逃,這就導(dǎo)致了內(nèi)存泄漏束铭。所以 Entry 以弱引用的方式避免了 ThreadLocal 沒有被回收而導(dǎo)致的內(nèi)存泄漏,但是此時(shí) value 仍然是無法回收的厢绝,依然會(huì)導(dǎo)致內(nèi)存泄漏契沫。

ThreadLocalMap 已經(jīng)考慮到這種情況,并且有一些防護(hù)措施:在調(diào)用 ThreadLocal 的 get()昔汉,set() 和 remove() 的時(shí)候都會(huì)清除當(dāng)前線程 ThreadLocalMap 中所有 key 為 null 的 value懈万。這樣可以降低內(nèi)存泄漏發(fā)生的概率。所以我們在使用 ThreadLocal 的時(shí)候靶病,每次用完 ThreadLocal 都調(diào)用 remove() 方法会通,清除數(shù)據(jù),防止內(nèi)存泄漏娄周。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末涕侈,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子昆咽,更是在濱河造成了極大的恐慌,老刑警劉巖牙甫,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掷酗,死亡現(xiàn)場離奇詭異,居然都是意外死亡窟哺,警方通過查閱死者的電腦和手機(jī)泻轰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來且轨,“玉大人浮声,你說我怎么就攤上這事⌒荩” “怎么了泳挥?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長至朗。 經(jīng)常有香客問我屉符,道長,這世上最難降的妖魔是什么锹引? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任矗钟,我火速辦了婚禮,結(jié)果婚禮上嫌变,老公的妹妹穿的比我還像新娘吨艇。我一直安慰自己,他們只是感情好腾啥,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布东涡。 她就那樣靜靜地躺著冯吓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪软啼。 梳的紋絲不亂的頭發(fā)上桑谍,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音祸挪,去河邊找鬼锣披。 笑死,一個(gè)胖子當(dāng)著我的面吹牛贿条,可吹牛的內(nèi)容都是我干的雹仿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼整以,長吁一口氣:“原來是場噩夢啊……” “哼胧辽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起公黑,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對情侶失蹤邑商,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后凡蚜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體人断,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年朝蜘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了恶迈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谱醇,死狀恐怖暇仲,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情副渴,我是刑警寧澤奈附,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站煮剧,受9級(jí)特大地震影響桅狠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜轿秧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一中跌、第九天 我趴在偏房一處隱蔽的房頂上張望凤价。 院中可真熱鬧恒削,春花似錦得滤、人聲如沸浙滤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽胀莹。三九已至孵淘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間闷沥,已是汗流浹背萎战。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留舆逃,地道東北人蚂维。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像路狮,于是被迫代替她去往敵國和親虫啥。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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