深入理解ThreadLocal

wuweishuo發表於2020-07-22

用途

我們一般用ThreadLocal來提供執行緒區域性變數。執行緒區域性變數會在每個Thread內擁有一個副本,Thread只能訪問自己的那個副本。文字解釋總是晦澀的,我們來看個例子。

public class Test {

    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        Thread thread1 = new MyThread("lucy");
        Thread thread2 = new MyThread("lily");
        thread1.start();
        thread2.start();
    }

    private static class MyThread extends Thread {

        MyThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            Thread thread = Thread.currentThread();
            threadLocal.set("i am " + thread.getName());
            try {
                //睡眠兩秒,確保執行緒lucy和執行緒lily都呼叫了threadLocal的set方法。
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(thread.getName() + " say: " + threadLocal.get());
        }
    }
}
複製程式碼

這個例子非常簡單,就是建立了lucy和lily兩個執行緒。線上程內部,呼叫threadLocal的set方法存入一字串,睡眠2秒後輸出執行緒名稱和threadLocal中的字串。我們執行這單程式碼,看一下輸出內容。

lucy say: i am lucy
lily say: i am lily
複製程式碼

原理

上面例子很好的解釋了ThreadLocal的作用,接下來我們分析一下這是如何實現的。

我們定位到ThreadLocal的set方法。原始碼中set方法被拆分為幾個方法,為了表述方便筆者將這幾個方法進行了整合。

public void set(T value) {
    //獲取當前執行緒
    Thread t = Thread.currentThread();
    //獲取當前執行緒的ThreadLocalMap
    ThreadLocalMap map = t.threadLocals;
    if (map != null)
        //將資料放入ThreadLocalMap中,key是當前ThreadLocal物件,值是我們傳入的value。
        map.set(this, value);
    else
        //初始化ThreadLocalMap,並以當前ThreadLocal物件為Key,value為值存入map中。
        t.threadLocals = new ThreadLocalMap(this, value);
}
複製程式碼

通過上面這段程式碼可以看到,ThreadLocal的set方法主要是通過當前執行緒的ThreadLocalMap實現的。ThreadLocalMap是一個Map,它的key是ThreadLoacl,value是Object。

TreadLocal的get方法的原始碼我就不貼出來了,大體上與set方法類似,就是先獲取到當前執行緒的ThreadLocalMap,然後以this為key可以取得value。

到這裡我們基本上明白了ThreadLocal的工作原理,我們總結一下

  1. 每個Thread例項內部都有一個ThreadLocalMap,ThreadLocalMap是一種Map,它的key是ThreadLocal,value是Object。
  2. ThreadLocal的set方法其實是往當前執行緒的ThreadLocalMap中存入資料,其key是當前ThreadLocal物件,value是set方法中傳入的值。
  3. 使用資料時,以當前ThreadLocal為key,從當前執行緒的ThreadLocalMap中取出資料。

ThreadLocalMap

上面我們介紹了ThreadLocal主要是通過執行緒的ThreadLocalMap實現的。

    static class ThreadLocalMap {
        private ThreadLocal.ThreadLocalMap.Entry[] table;

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

            Entry(ThreadLocal<?> var1, Object var2) {
                super(var1);
                this.value = var2;
            }
        }
    }
複製程式碼

ThreadLocalMap是一種Map,其內部維護著一個Entry[]。

ThreadLocalMap其實是就是將Key和Value包裝成Entry,然後放入Entry陣列中。我們看一下它的set方法。

        private void set(ThreadLocal<?> key, Object value) {
            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();
              
                if (k == key) {
                  //如果已經存在,直接替換value
                    e.value = value;
                    return;
                }

                if (k == null) {//如果當前位置的key ThreadLocal為空,替換key和value。下文ThreadLocal記憶體分析中會提到為什麼會有這段程式碼。
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);//該位置沒有資料,直接存入。
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold) //檢查是否擴容
                rehash();
        }

        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }
複製程式碼

到這裡,如果你瞭解HashMap,應該可以看出ThreadLocalMap就是一種HashMap。不過它並沒有採用java.util.HashMap中陣列+連結串列的方式解決Hash衝突,而是採用index後移的方式。

我們簡單分析一下這段程式碼:

  1. 通過ThreadLocal的threadLocalHashCode與當前Map的長度計算出陣列下標 i。

  2. 從i開始遍歷Entry陣列,這會有三種情況:

    • Entry的key就是我們要set的ThreadLocal,直接替換Entry中的value。

    • Entry的key為空,直接替換key和value。

    • 發生了Hash衝突,當前位置已經有了資料,查詢下一個可用空間。

  3. 找到沒有資料的位置,將key和value放入。

  4. 檢查是否擴容。

我們知道,HashMap是一種get、set都非常高效的集合,它的時間複雜度只有O(1)。但是如果存在嚴重的Hash衝突,那HashMap的效率就會降低很多。我們通過上段程式碼知道,ThreadLocalMap是通過 key.threadLocalHashCode & (len-1)計算Entry存放index的。len是當前Entry[]的長度,這沒什麼好說的。那看來祕密就在threadLocalHashCode中了。我們來看一下threadLocalHashCode是如何產生的。

public class ThreadLocal<T> {
  
    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);
    }
}
複製程式碼

這段程式碼非常簡單。有個全域性的計數器nextHashCode,每有一個ThreadLocal產生這個計數器就會加0x61c88647,然後把當前值賦給threadLocalHashCode。關於0x61c88647這個神奇的常量,可以點這裡

ThreadLocal記憶體分析

不知從何時起,網上開始流傳ThreadLocal有記憶體洩漏的問題。下面我們從ThreadLocal的記憶體入手,分析一下這種說法是否正確。話不多說直接上圖。

深入理解ThreadLocal

現在,我們假設ThreadLocal完成了自己的使命,與ThreadLocalRef斷開了引用關係。此時記憶體圖變成了這樣。

深入理解ThreadLocal

系統GC發生時,由於Heap中的ThreadLocal只有來自key的弱引用,因此ThreadLocal記憶體會被回收到。

深入理解ThreadLocal

到這裡,value被留在了Heap中,而我們沒辦法通過引用訪問它。value這塊記憶體將會持續到執行緒結束。如果不想依賴執行緒的生命週期,那就呼叫remove方法來釋放value的記憶體吧。個人認為,這種設計應該也是JDK開發大佬的無奈之舉。我們從原始碼中來感受一下這些大佬為了儘可能降低記憶體洩漏風險作出的努力。

  1. ThreadLocalMap.Entry軟引用ThreadLocal,避免了ThreadLocal的記憶體洩漏。

  2. 還記得ThreadLocalMap set方法中的這段程式碼嗎?

    private void set(ThreadLocal<?> key, Object value) {
    			
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
          
    		...
          
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    }
    
    複製程式碼
  3. ThreadLocal get方法獲取時,有一段如果Entry的key為null,移除Entry和Entry.value的程式碼。

    private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
    
        // expunge entry at staleSlot
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;
    
        // Rehash until we encounter 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--;
            }
          ...
        }
        return i;
    }
    複製程式碼

相關文章