阿里架構(gòu)師淺析ThreadLocal源碼——黃金分割數(shù)的使用

一. 前提

最近接觸到的一個(gè)項(xiàng)目要兼容新老系統(tǒng)燥翅,最終采用了ThreadLocal(實(shí)際上用的是InheritableThreadLocal)用于在子線程獲取父線程中共享的變量。問(wèn)題是解決了述雾,但是后來(lái)發(fā)現(xiàn)對(duì)ThreadLocal的理解不夠深入,于是順便把它的源碼閱讀理解了一遍卖怜。在談到ThreadLocal之前先買個(gè)關(guān)子耍鬓,先談?wù)匋S金分割數(shù)。本文在閱讀ThreadLocal源碼的時(shí)候是使用JDK8(1.8.0_181)吱肌。

二. 黃金分割數(shù)與斐波那契數(shù)列

首先復(fù)習(xí)一下斐波那契數(shù)列,下面的推導(dǎo)過(guò)程來(lái)自某搜索引擎的wiki:

  • 斐波那契數(shù)列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …
  • 通項(xiàng)公式:假設(shè)F(n)為該數(shù)列的第n項(xiàng)(n ∈ N*)仰禽,那么這句話可以寫成如下形式:F(n) = F(n-1) + F(n-2)氮墨。

有趣的是,這樣一個(gè)完全是自然數(shù)的數(shù)列吐葵,通項(xiàng)公式卻是用無(wú)理數(shù)來(lái)表達(dá)的规揪。而且當(dāng)n趨向于無(wú)窮大時(shí),前一項(xiàng)與后一項(xiàng)的比值越來(lái)越逼近0.618(或者說(shuō)后一項(xiàng)與前一項(xiàng)的比值小數(shù)部分越來(lái)越逼近0.618)折联,而這個(gè)值0.618就被稱為黃金分割數(shù)粒褒。證明過(guò)程如下:

黃金分割數(shù)的準(zhǔn)確值為(根號(hào)5 - 1)/2,約等于0.618诚镰。

三. 黃金分割數(shù)的應(yīng)用

黃金分割數(shù)被廣泛使用在美術(shù)奕坟、攝影等藝術(shù)領(lǐng)域,因?yàn)樗哂袊?yán)格的比例性清笨、藝術(shù)性月杉、和諧性,蘊(yùn)藏著豐富的美學(xué)價(jià)值抠艾,能夠激發(fā)人的美感苛萎。當(dāng)然,這些不是本文研究的方向检号,我們先嘗試求出無(wú)符號(hào)整型和帶符號(hào)整型的黃金分割數(shù)的具體值:

public static void main(String[] args) throws Exception {
    //黃金分割數(shù) * 2的32次方 = 2654435769 - 這個(gè)是無(wú)符號(hào)32位整數(shù)的黃金分割數(shù)對(duì)應(yīng)的那個(gè)值
    long c = (long) ((1L << 32) * (Math.sqrt(5) - 1) / 2);
    System.out.println(c);
    //強(qiáng)制轉(zhuǎn)換為帶符號(hào)為的32位整型腌歉,值為-1640531527
    int i = (int) c;
    System.out.println(i);
}

通過(guò)一個(gè)線段圖理解一下:

也就是2654435769為32位無(wú)符號(hào)整數(shù)的黃金分割值,而-1640531527就是32位帶符號(hào)整數(shù)的黃金分割值齐苛。而ThreadLocal中的哈希魔數(shù)正是1640531527(十六進(jìn)制為0x61c88647)翘盖。為什么要使用0x61c88647作為哈希魔數(shù)?這里提前說(shuō)一下ThreadLocal在ThreadLocalMap(ThreadLocal在ThreadLocalMap以Key的形式存在)中的哈希求Key下標(biāo)的規(guī)則:

哈希算法:keyIndex = ((i + 1) * HASH_INCREMENT) & (length - 1)

其中凹蜂,i為ThreadLocal實(shí)例的個(gè)數(shù)馍驯,這里的HASH_INCREMENT就是哈希魔數(shù)0x61c88647阁危,length為ThreadLocalMap中可容納的Entry(K-V結(jié)構(gòu))的個(gè)數(shù)(或者稱為容量)。在ThreadLocal中的內(nèi)部類ThreadLocalMap的初始化容量為16汰瘫,擴(kuò)容后總是2的冪次方狂打,因此我們可以寫個(gè)Demo模擬整個(gè)哈希的過(guò)程:

public class Main {
    
    private static final int HASH_INCREMENT = 0x61c88647;

    public static void main(String[] args) throws Exception {
        hashCode(4);
        hashCode(16);
        hashCode(32);
    }

    private static void hashCode(int capacity) throws Exception {
        int keyIndex;
        for (int i = 0; i < capacity; i++) {
            keyIndex = ((i + 1) * HASH_INCREMENT) & (capacity - 1);
            System.out.print(keyIndex);
            System.out.print(" ");
        }
        System.out.println();
    }
}

上面的例子中,我們分別模擬了ThreadLocalMap容量為4,16,32的情況下混弥,不觸發(fā)擴(kuò)容趴乡,并且分別”放入”4,16,32個(gè)元素到容器中,輸出結(jié)果如下:

3 2 1 0 
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0

每組的元素經(jīng)過(guò)散列算法后恰好填充滿了整個(gè)容器剑逃,也就是實(shí)現(xiàn)了完美散列浙宜。實(shí)際上,這個(gè)并不是偶然蛹磺,其實(shí)整個(gè)哈希算法可以轉(zhuǎn)換為多項(xiàng)式證明:證明(x - y) * HASH_INCREMENT != 2^n * (n m),在x != y同仆,n != m萤捆,HASH_INCREMENT為奇數(shù)的情況下恒成立,具體證明可以自行完成俗批。HASH_INCREMENT賦值為0x61c88647的API文檔注釋如下:

連續(xù)生成的哈希碼之間的差異(增量值)俗或,將隱式順序線程本地id轉(zhuǎn)換為幾乎最佳分布的乘法哈希值,這些不同的哈希值最終生成一個(gè)2的冪次方的哈希表岁忘。

四. ThreadLocal是什么

下面引用ThreadLocal的API注釋:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID)

稍微翻譯一下:ThreadLocal提供線程局部變量辛慰。這些變量與正常的變量不同,因?yàn)槊恳粋€(gè)線程在訪問(wèn)ThreadLocal實(shí)例的時(shí)候(通過(guò)其get或set方法)都有自己的干像、獨(dú)立初始化的變量副本帅腌。ThreadLocal實(shí)例通常是類中的私有靜態(tài)字段,使用它的目的是希望將狀態(tài)(例如麻汰,用戶ID或事務(wù)ID)與線程關(guān)聯(lián)起來(lái)速客。

ThreadLocal由Java界的兩個(gè)大師級(jí)的作者編寫,Josh Bloch和Doug Lea五鲫。Josh Bloch是JDK5語(yǔ)言增強(qiáng)溺职、Java集合(Collection)框架的創(chuàng)辦人以及《Effective Java》系列的作者。Doug Lea是JUC(java.util.concurrent)包的作者位喂,Java并發(fā)編程的泰斗浪耘。所以,ThreadLocal的源碼十分值得學(xué)習(xí)塑崖。

五. ThreadLocal的原理

ThreadLocal雖然叫線程本地(局部)變量七冲,但是實(shí)際上它并不存放任何的信息,可以這樣理解:它是線程(Thread)操作ThreadLocalMap中存放的變量的橋梁弃舒。它主要提供了初始化癞埠、set()状原、get()、remove()幾個(gè)方法苗踪。這樣說(shuō)可能有點(diǎn)抽象颠区,下面畫個(gè)圖說(shuō)明一下在線程中使用ThreadLocal實(shí)例的set()和get()方法的簡(jiǎn)單流程圖。

假設(shè)我們有如下的代碼通铲,主線程的線程名字是main(也有可能不是main):

public class Main {
    
    private static final ThreadLocal<String> LOCAL = new ThreadLocal<>();
    
    public static void main(String[] args) throws Exception{
        LOCAL.set("doge");
        System.out.println(LOCAL.get());
    }
}

上面只描述了單線程的情況并且因?yàn)槭侵骶€程忽略了Thread t = new Thread()這一步毕莱,如果有多個(gè)線程會(huì)稍微復(fù)雜一些,但是原理是不變的颅夺,ThreadLocal實(shí)例總是通過(guò)Thread.currentThread()獲取到當(dāng)前操作線程實(shí)例朋截,然后去操作線程實(shí)例中的ThreadLocalMap類型的成員變量,因此它是一個(gè)橋梁吧黄,本身不具備存儲(chǔ)功能部服。

六. ThreadLocal源碼分析

對(duì)于ThreadLocal的源碼,我們需要重點(diǎn)關(guān)注set()拗慨、get()廓八、remove()幾個(gè)方法。

1. ThreadLocal的內(nèi)部屬性

//獲取下一個(gè)ThreadLocal實(shí)例的哈希魔數(shù)
private final int threadLocalHashCode = nextHashCode();

//原子計(jì)數(shù)器赵抢,主要到它被定義為靜態(tài)
private static AtomicInteger nextHashCode = new AtomicInteger();

//哈希魔數(shù)(增長(zhǎng)數(shù))剧蹂,也是帶符號(hào)的32位整型值黃金分割值的取正
private static final int HASH_INCREMENT = 0x61c88647;

//生成下一個(gè)哈希魔數(shù)
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

這里需要注意一點(diǎn),threadLocalHashCode是一個(gè)final的屬性烦却,而原子計(jì)數(shù)器變量nextHashCode和生成下一個(gè)哈希魔數(shù)的方法nextHashCode()是靜態(tài)變量和靜態(tài)方法宠叼,靜態(tài)變量只會(huì)初始化一次。換而言之其爵,每新建一個(gè)ThreadLocal實(shí)例冒冬,它內(nèi)部的threadLocalHashCode就會(huì)增加0x61c88647。舉個(gè)例子:

//t1中的threadLocalHashCode變量為0x61c88647
ThreadLocal t1 = new ThreadLocal();
//t2中的threadLocalHashCode變量為0x61c88647 + 0x61c88647
ThreadLocal t2 = new ThreadLocal();
//t3中的threadLocalHashCode變量為0x61c88647 + 0x61c88647 + 0x61c88647
ThreadLocal t3 = new ThreadLocal();

threadLocalHashCode是下面的ThreadLocalMap結(jié)構(gòu)中使用的哈希算法的核心變量醋闭,對(duì)于每個(gè)ThreadLocal實(shí)例窄驹,它的threadLocalHashCode是唯一的。

2. 內(nèi)部類ThreadLocalMap的基本結(jié)構(gòu)和源碼分析

ThreadLocal內(nèi)部類ThreadLocalMap使用了默認(rèn)修飾符证逻,也就是包(包私有)可訪問(wèn)的乐埠。ThreadLocalMap內(nèi)部定義了一個(gè)靜態(tài)類Entry。我們重點(diǎn)看下ThreadLocalMap的源碼囚企,先看成員和結(jié)構(gòu)部分:

/**
 * ThreadLocalMap是一個(gè)定制的散列映射丈咐,僅適用于維護(hù)線程本地變量。
 * 它的所有方法都是定義在ThreadLocal類之內(nèi)龙宏。
 * 它是包私有的棵逊,所以在Thread類中可以定義ThreadLocalMap作為變量。
 * 為了處理非常大(指的是值)和長(zhǎng)時(shí)間的用途银酗,哈希表的Key使用了弱引用(WeakReferences)辆影。
 * 引用的隊(duì)列(弱引用)不再被使用的時(shí)候徒像,對(duì)應(yīng)的過(guò)期的條目就能通過(guò)主動(dòng)刪除移出哈希表。
 */
static class ThreadLocalMap {
 
        //注意這里的Entry的Key為WeakReference<ThreadLocal<?>>
    static class Entry extends WeakReference<ThreadLocal<?>> {
        
        //這個(gè)是真正的存放的值
        Object value;
                // Entry的Key就是ThreadLocal實(shí)例本身蛙讥,Value就是輸入的值
        Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
    }
        //初始化容量锯蛀,必須是2的冪次方
    private static final int INITIAL_CAPACITY = 16;

        //哈希(Entry)表,必須時(shí)擴(kuò)容次慢,長(zhǎng)度必須為2的冪次方
    private Entry[] table;

        //哈希表中元素(Entry)的個(gè)數(shù)
    private int size = 0;
 
        //下一次需要擴(kuò)容的閾值旁涤,默認(rèn)值為0
    private int threshold;
   
        //設(shè)置下一次需要擴(kuò)容的閾值,設(shè)置值為輸入值len的三分之二
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }
    
    // 以len為模增加i
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }
     
    // 以len為模減少i
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }
}

這里注意到十分重要的一點(diǎn):ThreadLocalMap$Entry是WeakReference(弱引用)迫像,并且鍵值Key為ThreadLocal<?>實(shí)例本身劈愚,這里使用了無(wú)限定的泛型通配符。

接著看ThreadLocalMap的構(gòu)造函數(shù):

// 構(gòu)造ThreadLocal時(shí)候使用闻妓,對(duì)應(yīng)ThreadLocal的實(shí)例方法void createMap(Thread t, T firstValue)
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 哈希表默認(rèn)容量為16
    table = new Entry[INITIAL_CAPACITY];
    // 計(jì)算第一個(gè)元素的哈希碼
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

// 構(gòu)造InheritableThreadLocal時(shí)候使用菌羽,基于父線程的ThreadLocalMap里面的內(nèi)容進(jìn)行提取放入新的ThreadLocalMap的哈希表中
// 對(duì)應(yīng)ThreadLocal的靜態(tài)方法static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap)
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
    // 基于父ThreadLocalMap的哈希表進(jìn)行拷貝
    for (Entry e : parentTable) {
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

這里注意一下,ThreadLocal的set()方法調(diào)用的時(shí)候會(huì)懶初始化一個(gè)ThreadLocalMap并且放入第一個(gè)元素由缆。而ThreadLocalMap的私有構(gòu)造是提供給靜態(tài)方法ThreadLocal#createInheritedMap()使用的算凿。

接著看ThreadLocalMap提供給ThreadLocal使用的一些實(shí)例方法:

// 如果Key在哈希表中找不到哈希槽的時(shí)候會(huì)調(diào)用此方法
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 這里會(huì)通過(guò)nextIndex嘗試遍歷整個(gè)哈希表,如果找到匹配的Key則返回Entry
    // 如果哈希表中存在Key == null的情況犁功,調(diào)用expungeStaleEntry進(jìn)行清理
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

// 1.清空staleSlot對(duì)應(yīng)哈希槽的Key和Value
// 2.對(duì)staleSlot到下一個(gè)空的哈希槽之間的所有可能沖突的哈希表部分槽進(jìn)行重哈希,置空Key為null的槽
// 3.注意返回值是staleSlot之后的下一個(gè)空的哈希槽的哈希碼
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    
    // expunge entry at staleSlot
    // 清空staleSlot對(duì)應(yīng)哈希槽的Key和Value
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    // 下面的過(guò)程是對(duì)staleSlot到下一個(gè)空的哈希槽之間的所有可能沖突的哈希表部分槽進(jìn)行重哈希婚夫,置空Key為null的槽
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

// 這里個(gè)方法比較長(zhǎng)浸卦,作用是替換哈希碼為staleSlot的哈希槽中Entry的值
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).
    int slotToExpunge = staleSlot;
    // 這個(gè)循環(huán)主要是為了找到staleSlot之前的最前面的一個(gè)Key為null的哈希槽的哈希碼
    for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // Find either the key or trailing null slot of run, whichever
    // occurs first
    // 遍歷staleSlot之后的哈希槽,如果Key匹配則用輸入值替換
    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        if (k == key) {
            e.value = value;
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
    // Key匹配不了案糙,則新創(chuàng)建一個(gè)哈希槽
    // If key not found, put new entry in stale slot
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
    
    // 這里如果當(dāng)前的staleSlot和找到前置的slotToExpunge不一致會(huì)進(jìn)行一次清理
    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

// 對(duì)當(dāng)前哈希表中所有的Key為null的Entry調(diào)用expungeStaleEntry
private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

// 清理第i個(gè)哈希槽之后的n個(gè)哈希槽限嫌,如果遍歷的時(shí)候發(fā)現(xiàn)Entry的Key為null,則n會(huì)重置為哈希表的長(zhǎng)度时捌,expungeStaleEntry有可能會(huì)重哈希使得哈希表長(zhǎng)度發(fā)生變化
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}


/**
 * 這個(gè)方法主要給`ThreadLocal#get()`調(diào)用怒医,通過(guò)當(dāng)前ThreadLocal實(shí)例獲取哈希表中對(duì)應(yīng)的Entry
 *
 */
private Entry getEntry(ThreadLocal<?> key) {
    // 計(jì)算Entry的哈希值
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i]; 
    if (e != null && e.get() == key)
        return e;
    else  // 注意這里,如果e為null或者Key對(duì)不上奢讨,會(huì)調(diào)用getEntryAfterMiss
        return getEntryAfterMiss(key, i, e);
}

// 重哈希稚叹,必要時(shí)進(jìn)行擴(kuò)容
private void rehash() {
    // 清理所有空的哈希槽,并且進(jìn)行重哈希
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    // 哈希表的哈希元素個(gè)數(shù)大于3/4閾值時(shí)候觸發(fā)擴(kuò)容
    if (size >= threshold - threshold / 4)
        resize();
}

// 擴(kuò)容拿诸,簡(jiǎn)單的擴(kuò)大2倍的容量        
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (Entry e : oldTab) {
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                     h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}

// 基于ThreadLocal作為key扒袖,對(duì)當(dāng)前的哈希表設(shè)置值,此方法由`ThreadLocal#set()`調(diào)用
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    // 變量哈希表
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        // Key匹配亩码,直接設(shè)置值
        if (k == key) {
            e.value = value;
            return;
        }
        // 如果Entry的Key為null季率,則替換該Key為當(dāng)前的key,并且設(shè)置值
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 清理當(dāng)前新設(shè)置元素的哈希槽下標(biāo)到sz段的哈希槽描沟,如果清理成功并且sz大于閾值則觸發(fā)擴(kuò)容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

簡(jiǎn)單來(lái)說(shuō)飒泻,ThreadLocalMap是ThreadLocal真正的數(shù)據(jù)存儲(chǔ)容器鞭光,實(shí)際上ThreadLocal數(shù)據(jù)操作的復(fù)雜部分的所有邏輯都在ThreadLocalMap中進(jìn)行,而ThreadLocalMap實(shí)例是Thread的成員變量泞遗,在ThreadLocal#set()方法首次調(diào)用的時(shí)候設(shè)置到當(dāng)前執(zhí)行的線程實(shí)例中惰许。如果在同一個(gè)線程中使用多個(gè)ThreadLocal實(shí)例,實(shí)際上刹孔,每個(gè)ThreadLocal實(shí)例對(duì)應(yīng)的是ThreadLocalMap的哈希表中的一個(gè)哈希槽啡省。舉個(gè)例子,在主函數(shù)主線程中使用多個(gè)ThreadLocal實(shí)例:

public class ThreadLocalMain {

    private static final ThreadLocal<Integer> TL_1 = new ThreadLocal<>();
    private static final ThreadLocal<String> TL_2 = new ThreadLocal<>();
    private static final ThreadLocal<Long> TL_3 = new ThreadLocal<>();

    public static void main(String[] args) throws Exception {
        TL_1.set(1);
        TL_2.set("1");
        TL_3.set(1L);
        Field field = Thread.class.getDeclaredField("threadLocals");
        field.setAccessible(true);
        Object o = field.get(Thread.currentThread());
        System.out.println(o);
    }
}

實(shí)際上髓霞,主線程的threadLocals屬性中的哈希表中一般不止我們上面定義的三個(gè)ThreadLocal卦睹,因?yàn)榧虞d主線程的時(shí)候還有可能在其他地方使用到ThreadLocal,筆者某次Debug的結(jié)果如下:

用PPT畫圖簡(jiǎn)化一下:

上圖threadLocalHashCode屬性一行的表是為了標(biāo)出每個(gè)Entry的哈希槽的哈希值方库,實(shí)際上结序,threadLocalHashCode是ThreadLocal@XXXX中的一個(gè)屬性,這是很顯然的纵潦,本來(lái)threadLocalHashCode就是ThreadLocal的一個(gè)成員變量徐鹤。

上面只是簡(jiǎn)單粗略對(duì)ThreadLocalMap的源碼進(jìn)行了流水賬的分析,下文會(huì)作一些詳細(xì)的圖邀层,說(shuō)明一下ThreadLocal和ThreadLocalMap中的一些核心操作的過(guò)程返敬。

3. ThreadLocal的創(chuàng)建

從ThreadLocal的構(gòu)造函數(shù)來(lái)看,ThreadLocal實(shí)例的構(gòu)造并不會(huì)做任何操作寥院,只是為了得到一個(gè)ThreadLocal的泛型實(shí)例劲赠,后續(xù)可以把它作為ThreadLocalMap$Entry的鍵:

// 注意threadLocalHashCode在每個(gè)新`ThreadLocal`實(shí)例的構(gòu)造同時(shí)已經(jīng)確定了
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

// 通過(guò)Supplier去覆蓋initialValue方法
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

// 默認(rèn)公有構(gòu)造函數(shù)
public ThreadLocal() {

}

注意threadLocalHashCode在每個(gè)新ThreadLocal實(shí)例的構(gòu)造同時(shí)已經(jīng)確定了,這個(gè)值也是Entry哈希表的哈希槽綁定的哈希值秸谢。

4. TreadLocal的set方法

ThreadLocal中set()方法的源碼如下:

public void set(T value) {
    //設(shè)置值前總是獲取當(dāng)前線程實(shí)例
    Thread t = Thread.currentThread();
    //從當(dāng)前線程實(shí)例中獲取threadLocals屬性
    ThreadLocalMap map = getMap(t);
    if (map != null)
         //threadLocals屬性不為null則覆蓋key為當(dāng)前的ThreadLocal實(shí)例凛澎,值為value
         map.set(this, value);
    else
    //threadLocals屬性為null,則創(chuàng)建ThreadLocalMap估蹄,第一個(gè)項(xiàng)的Key為當(dāng)前的ThreadLocal實(shí)例塑煎,值為value
        createMap(t, value);
}

// 這里看到獲取ThreadLocalMap實(shí)例時(shí)候總是從線程實(shí)例的成員變量獲取
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 創(chuàng)建ThreadLocalMap實(shí)例的時(shí)候,會(huì)把新實(shí)例賦值到線程實(shí)例的threadLocals成員
void createMap(Thread t, T firstValue) {
     t.threadLocals = new ThreadLocalMap(this, firstValue);
}

上面的過(guò)程源碼很簡(jiǎn)單臭蚁,設(shè)置值的時(shí)候總是先獲取當(dāng)前線程實(shí)例并且操作它的變量threadLocals最铁。步驟是:

  • 獲取當(dāng)前運(yùn)行線程的實(shí)例。
  • 通過(guò)線程實(shí)例獲取線程實(shí)例成員threadLocals(ThreadLocalMap)刊棕,如果為null炭晒,則創(chuàng)建一個(gè)新的ThreadLocalMap實(shí)例賦值到threadLocals。
  • 通過(guò)threadLocals設(shè)置值value甥角,如果原來(lái)的哈希槽已經(jīng)存在值网严,則進(jìn)行覆蓋。

5. TreadLocal的get方法

ThreadLocal中g(shù)et()方法的源碼如下:

 public T get() {
   //獲取當(dāng)前線程的實(shí)例
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    //根據(jù)當(dāng)前的ThreadLocal實(shí)例獲取ThreadLocalMap中的Entry嗤无,使用的是ThreadLocalMap的getEntry方法
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T) e.value;
             return result;
            }
        }
    //線程實(shí)例中的threadLocals為null震束,則調(diào)用initialValue方法怜庸,并且創(chuàng)建ThreadLocalMap賦值到threadLocals
    return setInitialValue();
}

private T setInitialValue() {
    // 調(diào)用initialValue方法獲取值
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // ThreadLocalMap如果未初始化則進(jìn)行一次創(chuàng)建,已初始化則直接設(shè)置值
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

protected T initialValue() {
     return null;
}

initialValue()方法默認(rèn)返回null垢村,如果ThreadLocal實(shí)例沒(méi)有使用過(guò)set()方法直接使用get()方法割疾,那么ThreadLocalMap中的此ThreadLocal為Key的項(xiàng)會(huì)把值設(shè)置為initialValue()方法的返回值。如果想改變這個(gè)邏輯可以對(duì)initialValue()方法進(jìn)行覆蓋嘉栓。

6. TreadLocal的remove方法

ThreadLocal中remove()方法的源碼如下:

public void remove() {
    //獲取Thread實(shí)例中的ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
       //根據(jù)當(dāng)前ThreadLocal作為Key對(duì)ThreadLocalMap的元素進(jìn)行移除
       m.remove(this);
}

七. ThreadLocal.ThreadLocalMap的初始化

我們可以關(guān)注一下java.lang.Thread類里面的變量:

public class Thread implements Runnable {

  //傳遞ThreadLocal中的ThreadLocalMap變量
  ThreadLocal.ThreadLocalMap threadLocals = null;
  //傳遞InheritableThreadLocal中的ThreadLocalMap變量
  ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

也就是宏榕,ThreadLocal需要存放和獲取的數(shù)據(jù)實(shí)際上綁定在Thread實(shí)例的成員變量threadLocals中,并且是ThreadLocal#set()方法調(diào)用的時(shí)候才進(jìn)行懶加載的侵佃,可以結(jié)合上一節(jié)的內(nèi)容理解一下麻昼,這里不展開。

八. 什么情況下ThreadLocal的使用會(huì)導(dǎo)致內(nèi)存泄漏

其實(shí)ThreadLocal本身不存放任何的數(shù)據(jù)馋辈,而ThreadLocal中的數(shù)據(jù)實(shí)際上是存放在線程實(shí)例中抚芦,從實(shí)際來(lái)看是線程內(nèi)存泄漏,底層來(lái)看是Thread對(duì)象中的成員變量threadLocals持有大量的K-V結(jié)構(gòu)迈螟,并且線程一直處于活躍狀態(tài)導(dǎo)致變量threadLocals無(wú)法釋放被回收叉抡。threadLocals持有大量的K-V結(jié)構(gòu)這一點(diǎn)的前提是要存在大量的ThreadLocal實(shí)例的定義,一般來(lái)說(shuō)答毫,一個(gè)應(yīng)用不可能定義大量的ThreadLocal褥民,所以一般的泄漏源是線程一直處于活躍狀態(tài)導(dǎo)致變量threadLocals無(wú)法釋放被回收。但是我們知道洗搂,·ThreadLocalMap·中的Entry結(jié)構(gòu)的Key用到了弱引用(·WeakReference<ThreadLocal<?>>·)轴捎,當(dāng)沒(méi)有強(qiáng)引用來(lái)引用ThreadLocal實(shí)例的時(shí)候,JVM的GC會(huì)回收ThreadLocalMap中的這些Key蚕脏,此時(shí),ThreadLocalMap中會(huì)出現(xiàn)一些Key為null侦锯,但是Value不為null的Entry項(xiàng)驼鞭,這些Entry項(xiàng)如果不主動(dòng)清理,就會(huì)一直駐留在ThreadLocalMap中尺碰。也就是為什么ThreadLocal中g(shù)et()挣棕、set()、remove()這些方法中都存在清理ThreadLocalMap實(shí)例key為null的代碼塊亲桥÷逍模總結(jié)下來(lái),內(nèi)存泄漏可能出現(xiàn)的地方是:

  • 大量地(靜態(tài))初始化ThreadLocal實(shí)例题篷,初始化之后不再調(diào)用get()词身、set()、remove()方法番枚。

  • 初始化了大量的ThreadLocal法严,這些ThreadLocal中存放了容量大的Value损敷,并且使用了這些ThreadLocal實(shí)例的線程一直處于活躍的狀態(tài)。

ThreadLocal中一個(gè)設(shè)計(jì)亮點(diǎn)是ThreadLocalMap中的Entry結(jié)構(gòu)的Key用到了弱引用深啤。試想如果使用強(qiáng)引用拗馒,等于ThreadLocalMap中的所有數(shù)據(jù)都是與Thread的生命周期綁定,這樣很容易出現(xiàn)因?yàn)榇罅烤€程持續(xù)活躍導(dǎo)致的內(nèi)存泄漏溯街。使用了弱引用的話诱桂,JVM觸發(fā)GC回收弱引用后,ThreadLocal在下一次調(diào)用get()呈昔、set()挥等、remove()方法就可以刪除那些ThreadLocalMap中Key為null的值,起到了惰性刪除釋放內(nèi)存的作用韩肝。

其實(shí)ThreadLocal在設(shè)置內(nèi)部類ThreadLocal.ThreadLocalMap中構(gòu)建的Entry哈希表已經(jīng)考慮到內(nèi)存泄漏的問(wèn)題触菜,所以ThreadLocal.ThreadLocalMap$Entry類設(shè)計(jì)為弱引用,類簽名為static class Entry extends WeakReference<ThreadLocal<?>>哀峻。之前一篇文章介紹過(guò)涡相,如果弱引用關(guān)聯(lián)的對(duì)象如果置為null,那么該弱引用會(huì)在下一次GC時(shí)候回收弱引用關(guān)聯(lián)的對(duì)象剩蟀。舉個(gè)例子:

public class ThreadLocalMain {

    private static ThreadLocal<Integer> TL_1 = new ThreadLocal<>();

    public static void main(String[] args) throws Exception {
        TL_1.set(1);
        TL_1 = null;
        System.gc();
        Thread.sleep(300);
    }
}

這種情況下催蝗,TL_1這個(gè)ThreadLocal在主動(dòng)GC之后,線程綁定的ThreadLocal.ThreadLocalMap實(shí)例中的Entry哈希表中原來(lái)的TL_1所在的哈希槽Entry的引用持有值referent(繼承自WeakReference)會(huì)變成null育特,但是Entry中的value是強(qiáng)引用丙号,還存放著TL_1這個(gè)ThreadLocal未回收之前的值。這些被”孤立”的哈希槽Entry就是前面說(shuō)到的要惰性刪除的哈希槽缰冤。

九. ThreadLocal的最佳實(shí)踐

其實(shí)ThreadLocal的最佳實(shí)踐很簡(jiǎn)單:

  • 每次使用完ThreadLocal實(shí)例犬缨,都調(diào)用它的remove()方法,清除Entry中的數(shù)據(jù)棉浸。

調(diào)用remove()方法最佳時(shí)機(jī)是線程運(yùn)行結(jié)束之前的finally代碼塊中調(diào)用怀薛,這樣能完全避免操作不當(dāng)導(dǎo)致的內(nèi)存泄漏,這種主動(dòng)清理的方式比惰性刪除有效迷郑。

十. 小結(jié)

ThreadLocal線程本地變量是線程實(shí)例傳遞和存儲(chǔ)共享變量的橋梁枝恋,真正的共享變量還是存放在線程實(shí)例本身的屬性中。ThreadLocal里面的基本邏輯并不復(fù)雜嗡害,但是一旦涉及到性能影響焚碌、內(nèi)存回收(弱引用)和惰性刪除等環(huán)節(jié),其實(shí)它考慮到的東西還是相對(duì)全面而且有效的霸妹。

寫在最后

  • 第一:看完點(diǎn)贊十电,感謝您的認(rèn)可;
  • ...
  • 第二:隨手轉(zhuǎn)發(fā),分享知識(shí)摆出,讓更多人學(xué)習(xí)到朗徊;
  • ...
  • 第三:記得點(diǎn)關(guān)注,每天更新的Y寺R摇!
  • ...
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末象踊,一起剝皮案震驚了整個(gè)濱河市温亲,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌杯矩,老刑警劉巖栈虚,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異史隆,居然都是意外死亡魂务,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門泌射,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)粘姜,“玉大人,你說(shuō)我怎么就攤上這事熔酷」陆簦” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵拒秘,是天一觀的道長(zhǎng)号显。 經(jīng)常有香客問(wèn)我,道長(zhǎng)躺酒,這世上最難降的妖魔是什么押蚤? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮羹应,結(jié)果婚禮上活喊,老公的妹妹穿的比我還像新娘。我一直安慰自己量愧,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布帅矗。 她就那樣靜靜地躺著偎肃,像睡著了一般。 火紅的嫁衣襯著肌膚如雪浑此。 梳的紋絲不亂的頭發(fā)上累颂,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼紊馏。 笑死料饥,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的朱监。 我是一名探鬼主播岸啡,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼赫编!你這毒婦竟也來(lái)了巡蘸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤擂送,失蹤者是張志新(化名)和其女友劉穎悦荒,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嘹吨,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡搬味,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蟀拷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碰纬。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖匹厘,靈堂內(nèi)的尸體忽然破棺而出嘀趟,到底是詐尸還是另有隱情,我是刑警寧澤愈诚,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布她按,位于F島的核電站,受9級(jí)特大地震影響炕柔,放射性物質(zhì)發(fā)生泄漏酌泰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一匕累、第九天 我趴在偏房一處隱蔽的房頂上張望陵刹。 院中可真熱鬧,春花似錦欢嘿、人聲如沸衰琐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)羡宙。三九已至,卻和暖如春掐隐,著一層夾襖步出監(jiān)牢的瞬間狗热,已是汗流浹背钞馁。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留匿刮,地道東北人僧凰。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像熟丸,于是被迫代替她去往敵國(guó)和親训措。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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