天天看點

ThreadLocal常用方法淺析、使用場景及注意事項

1. ThreadLocal詳解

JDK1.2版本起,Java就提供了java.lang.ThreadLocal,ThreadLocal為每個使用線程都提供獨立的變量副本,可以做到線程間的資料隔離,每個線程都可以通路各自内部的副本變量。

線程上下文ThreadLocal又稱為"線程保險箱",ThreadLocal能夠将指定的變量和目前線程進行綁定,線程之間彼此隔離,持有不同的對象執行個體,進而避免了資料資源的競争。

2. ThreadLocal的使用場景

  1. 在進行對象跨層傳遞的時候,可以考慮ThreadLocal,避免方法多次傳遞,打破層次間的限制。
  2. 線程間資料隔離。
  3. 進行事務操作,用于儲存線程事務資訊。

注意:

ThreadLocal并不是解決多線程下共享資源的一種技術,一般情況下,每一個線程的ThreadLocal存儲的都是一個全新的對象(通過new關鍵字建立),如果多線程的ThreadLocal存儲了一個對象引用,那麼就會面臨資源競争,資料不一緻等并發問題。

3.常用方法源碼解析

3.1 initialValue方法

protected T initialValue() {
        return null;
 }
           

此方法為ThreadLocal儲存的資料類型指定的一個初始化值,在ThreadLocal中預設傳回null。但可以重寫initialValue()方法進行資料初始化。

如果使用的是Java8提供的Supplier函數接口更加簡化:

// withInitial()實際是建立了一個ThreadLocal的子類SuppliedThreadLocal,重寫initialValue()
ThreadLocal<Object> threadLocal = ThreadLocal.withInitial(Object::new);
           

3.2 set(T value)方法

主要存儲指定資料。

public void set(T value) {
    //	擷取目前線程Thread.currentThread() 
    Thread t = Thread.currentThread();
    // 根據目前線程擷取與之關聯的ThreadLocalMap資料結構
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 核心方法。set 周遊整個Entry的過程,後面有詳解
        map.set(this, value);
    else {
        // 調用createMap(),建立ThreadLocalMap,key為目前ThreadLocal執行個體,存入資料為目前value。
        // ThreadLocal會建立一個預設長度為16Entry節點,并将k-v放入i位置(i位置計算方式和hashmap相似,
        // 目前線程的hashCode&(entry預設長度-1)),并設定門檻值(預設為0)為Entry預設長度的2/3。
        createMap(t, value);
    }
}

// set 周遊整個Entry的過程
private void set(ThreadLocal<?> key, Object value) {
   	// 擷取所有的Entry
    Entry[] tab = table;
    int len = tab.length;
    // 根據ThreadLocal對象,計算角标位置
    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) {
            e.value = value;
            return;
        }
		// 如果ThreadLocal為null,直接驅出并使用新資料(Value)占居原來位置,
		// 這個過程主要是防止記憶體洩漏。
        if (k == null) {
            // 驅除ThreadLocal為null的Entry,并放入Value,這也是記憶體洩漏的重點地區
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	// entry都為null,建立新的entry,已ThreadLocal為key,将存放資料為Value。
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // ThreadLoaclMapde的目前資料元素的個數和門檻值比較,再次進行key為null的清理工作。
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        // 整理Entry,當Entry中的ThreadLocal對象為null時,通過重新計算角标位來清理
        // 以前ThreadLocal。如果Entry數量大于3/4容量進行擴容
        rehash();
}
           

3.3 get方法

get()用于傳回目前線程ThreadLocal中資料備份,目前線程的資料都存在一個ThreadLocalMap的資料結構中。

public T get() {
    Thread t = Thread.currentThread();
    // 獲得ThreadLocalMap對象map,ThreadLocalMap是和目前Thread關聯的,
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 存入ThreadLocal中的資料實際上是存儲在ThreadLocalMap的Entry中。
        // 而此Entry是放在一個Entry數組裡面的。
        // 擷取目前ThreadLocal對應的entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            // 直接傳回目前資料 
            T result = (T)e.value;
            return result;
        }
    }
    // ThreadLocalMap未初始化,首先初始化
    return setInitialValue();
}

// ThreadLocal的setInitialValue方法源碼
private T setInitialValue() {
    // 為ThreadLocalMap指定Value的初始化值
    T value = initialValue();
    Thread t = Thread.currentThread();
    //	根據本地線程Thread擷取ThreadLocalMap,一下方法與Set方法相同。
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 如果map存在,直接調用set()方法進行指派。
        map.set(this, value);
    else
        // map==null;建立ThreadLocalMap對象,并将Thread和value關聯起來
        createMap(t, value);
    return value;
}

           

3.4 小結

  1. initialValue() : 初始化ThreadLocal中的value屬性值。
  2. set():擷取目前線程,根據目前線程從ThreadLocals中擷取ThreadLocalMap資料結構,
    • 如果ThreadLocalmap的資料結構沒建立,則建立ThreadLocalMap,key為目前ThreadLocal執行個體,存入資料為目前value。ThreadLocal會建立一個預設長度為16Entry節點,并将k-v放入i位置(i位置計算方式和hashmap相似,目前線程的hashCode&(entry預設長度-1)),并設定門檻值(預設為0)為Entry預設長度的2/3。
    • 如果ThreadLocalMap存在。就會周遊整個Map中的Entry節點,如果entry中的key和本線程ThreadLocal相同,将資料(value)直接覆寫,并傳回。如果ThreadLoca為null,驅除ThreadLocal為null的Entry,并放入Value,這也是記憶體洩漏的重點地區。
  3. get()

    get()方法比較簡單。就是根據Thread擷取ThreadLocalMap。通過ThreadLocal來獲得資料value。注意的是:如果ThreadLocalMap沒有建立,直接進入建立過程。初始化ThreadLocalMap。并直接調用和set方法一樣的方法。

3.4 ThreadLocalMap資料結構

set()還是get()方法都是避免不了和ThreadLocalMap和Entry打交道。ThreadLocalMap是一個類似于HashMap的一個資料結構(沒有連結清單),僅僅用于存放線程存放在ThreadLocal中的資料備份,ThreadLocalMap的所有方法對外部都是不可見的。

ThreadLocalMap中用于存儲資料的Entry,它是一個WeakReference類型的子類,之是以設計成WeakReference是為了能夠是JVM發生gc,能夠自動回收,防止記憶體溢出現象。

4. ThreadLocal的副作用

4.1 ThreadLocal引起髒資料

線程複用會産生髒資料。

由于結程池會重用 Thread 對象 ,那麼與 Thread 綁定的類的靜态屬性 ThreadLocal 變量也會被重用。如果在實作的線程 run()方法體中不顯式地調用 remove() 清理與線程相關的ThreadLocal 資訊,那麼如果下一個線程不調用set()設定初始值,就可能 get()到重用的線程資訊,包括 ThreadLocal 所關聯的線程對象的 value 值。

// java.lang.Thread#threadLocals

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

4.2 ThreadLocal引起的記憶體洩漏

  1. 在上面提到ThreadLocalMap中存放的Entry是WeakReference的子類。是以在JVM觸發GC(young gc,Full GC)時,都會導緻Entry的回收
  2. 在get資料的時候,增加檢查,清除已經被回收器回收的Entry(WeakReference可以自動回收)
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        ThreadLocal<?> k = e.get();
      ...
        if (k == null)
            // 清除 key 是 null 的Entry
            expungeStaleEntry(i);
      ...
     return null;
    }
    
    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;
                // 清除key==null 的Entry
                i = expungeStaleEntry(i);
            }
        } while ( (n >>>= 1) != 0);
        return removed;
    }
    ```
               
  3. set資料時增加檢查,删除已經被垃圾回收器清理的Entry,并将其移除
    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;
                // 清除key==null 的Entry
                i = expungeStaleEntry(i);
            }
        } while ( (n >>>= 1) != 0);
        return removed;
    }
               

基于上面三點:ThreadLocal在一定程度上保證不會發生記憶體洩漏。但是Thread類中有ThreadlocalMap的引用,導緻對象的可達性,故不能回收。

ThreadLocal被置為null清除了。但是通過ThreadLocalMap還是被Thread類引用。導緻該資料是可達的。是以記憶體得不到釋放,除非目前線程結束,Thread引用就會被垃圾回收器回收。如圖所示

ThreadLocal常用方法淺析、使用場景及注意事項
ThreadLocal常用方法淺析、使用場景及注意事項

5 ThreadLocal記憶體洩漏解決方案及remove方法源碼解析

解決ThreadLocal記憶體洩漏的常用方法是:在使用完ThreadLocal之後,及時remove掉。

public void remove() {
    // 根據目前線程,擷取ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        // map不為null,執行remove操作
        m.remove(this);
}

// ThreadLocal 的remove()
private void remove(ThreadLocal<?> key) {
    // 擷取存放key-value的數組。
    Entry[] tab = table;
    int len = tab.length;
    // 根據ThreadLocal的HashCode确定唯一的角标
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            // 如果和本ThreadLocal相同。将引用置null。
            e.clear();
            // 實行Enty和Entry.value置null。源碼中 tab[staleSlot].value = null; tab[staleSlot] = null;
            expungeStaleEntry(i);
            return;
        }
    }
}
           

繼續閱讀