您有一份ThreadLocal完全解析手冊

jsbintask發表於2019-05-27

本文原創地址,:jsbintask的部落格(食用效果最佳),轉載請註明出處!

前言

ThreadLocal是jdk中一個非常重要的工具,它可以控制堆記憶體中的物件只能被指定執行緒訪問,如果你經常閱讀原始碼,基本在各大框架都能發現它的蹤影。而它最經典的應用就是事務管理,同時它也是面試中的常客。

原理

我們知道,堆記憶體是共享的,為什麼ThreadLocal能夠控制指定執行緒訪問呢? 如圖:

ThreadLocal

  1. 呼叫ThreadLocal的get方法。
  2. 獲取當前執行緒t1.
  3. 獲取t1的成員變數ThreadLocalMap
  4. 根據ThreadLocal的hashcode計算出ThreadLocalMap中Entry[]陣列的索引。
  5. 返回索引位置的值。 這樣我們就很容易理解了,為什麼只有當前執行緒才能獲取到某些值,因為這是這些值都直接儲存在當前執行緒的成員變數ThreadLocalMap中,而ThreadLocal在這個過程中充當的角色則是提供它獨一無二的hashcode值,這樣我們就能計算出我們儲存的值在ThreadLocalMap的位置。

原始碼分析

我們從構建一個ThreadLocal到呼叫它的set,get方法完整的分析一遍它的原始碼。

構造器

當我們使用new ThreadLocal<>() new一個ThreadLocal物件時,它初始化了一個成員變數threadLocalHashCode,這個成員變數代表當前ThreadLocal的hashcode值,而它肯定是唯一的:

ThreadLocal

  1. ThreadLocal內部有一個靜態hashCode生成器nextHashCode
  2. 每次新new一個ThreadLocal物件,呼叫這個生成器同步方法獲取hashcode。 因為依賴於靜態成員變數nextHashCode的關係,所以它的hashcode肯定唯一!

set(T t)

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
複製程式碼
  1. 獲取當前執行緒t。
  2. 從t中獲取ThreadLocalMap map。
    ThreadLocal
  3. 如果map不為空,將當前值value放入map。
  4. 如果map為空,新建一個ThreadLocalMap放入執行緒t。 ThreadLocalMap是ThreadLocal中的內部類,它的結構如下:
public class ThreadLocalMap {
    
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

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

    private Entry[] table;
    
    private int size = 0;
    
    private static final int INITIAL_CAPACITY = 16;
    
    private int threshold; // Default to 0
}
複製程式碼

類似於ArrayList內部的構造,它內部有一個Entry陣列table,並且Entry繼承自弱引用(gc時儲存的ThreadLocal會被標記清理),所以每一個Entry中儲存著兩個值,ThreadLocal,value,value既是我們要儲存的值。 接著,我們回過頭詳細分析第三步,ThreadLocalMap的set方法:

private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);  // 1

            for (Entry e = tab[i];
                 e != null;         // 2
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {   // 3
                    e.value = value;
                    return;
                }

                if (k == null) {   // 4
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);  // 5
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)   // 6
                rehash();
        }
複製程式碼
  1. 根據ThreadLocal的hashCode計算出在entry中的索引i。
  2. 取出i對應的Entry值e。
  3. 如果e的key等於當前ThreadLocal,代表已經有一個一樣的ThreadLocal在這個entry設值,直接替換這個entry上的value。
  4. e上面的ThreadLocal為null,代表垃圾收集器準備回收這個Entry了,重新計算陣列大小,重新hash。
  5. i位置還沒有初始化(第一次set這個ThreadLocal),直接將value放到i的位置。
  6. 擴容Entry陣列。

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();
}
複製程式碼
  1. 獲取當前執行緒。
  2. 從當前執行緒中獲取ThreadLocalMap
  3. 從ThreadLocalMap中找出ThreadLocal對應的Entry.
  4. 如果Entry不為null,直接返回Entry中的value
  5. 返回初始值。 其中,ThreadLocalMap的get(ThreadLocal tl)如下:
    ThreadLocal
    它和我們一開始的分析一樣,根據ThreadLocal的hashcode成員變數計算出索引位置i,得到Entry。這裡同樣有特殊情況,如果得到的Entry的key和當前ThreadLocal不相等,代表這個Entry將被垃圾收集處理,呼叫getEntryAfterMiss rehash,計算陣列大小。

注意事項

從上面的程式碼分析中,我們知道,ThreadLocalMap的生命週期和當前執行緒同步,如果當前執行緒被銷燬,則map中的所有引用均被銷燬。但如果當前執行緒不被銷燬呢(執行緒池,tomcat處理請求等)?Entry中儲存了ThreadLocal的弱引用以及value,gc時可能清理掉ThreadLocal,而這個value確再沒有訪問之地,這個時候就會造成記憶體洩漏! 所以我們需要手動呼叫remove方法清理掉當前執行緒ThreadLocalMap的引用!

總結

  1. ThreadLocal中真正儲存的值還是線上程的ThreadLocalMap中,ThreadLocal只是使用它的hashcode值充當中間計算變數。
  2. ThreadLocalMap內部使用一個Entry陣列儲存資料,它根據ThreadLocal計算出來的hashcode得到Entry的索引值,而ThreadLocal的hashcod在其內部的靜態工具類產生,所以不會出現衝突。
  3. 線上程池模式下,ThreadLocal生命週期伴隨著執行緒一直存在,可能出現記憶體洩漏的情況,最好手動呼叫remove方法。

關注我,這裡只有乾貨!

相關文章