JavaSE多線程-ThreadLocal原理(源碼分析)

關於Thread類:

Thread類中維護了ThreadLocal.ThreadLocalMap屬性,這就是每個線程的存儲空間。相當於Map,key爲當前線程,value爲entry。

Public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
 * InheritableThreadLocal values pertaining to this thread. This map is
 * maintained by the InheritableThreadLocal class.
 */
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

關於ThreadLocal:

ThreadLocal提供一個線程局部變量(thread local variable),訪問到某個變量的每個線程都有自己的局部變量。提供線程保持對象的方法和避免參數傳遞的方便的對象訪問方式

Thread是一個空間換時間的方案,爲每個線程的中併發訪問的數據提供一個副本,通過訪問副本來運行業務,耗費了內存但減少線程同步所帶來的線程消耗

每個線程可訪問自己內部的副本變量。這樣做的好處是可以保證共享變量在多線程環境下訪問的線程安全性

 

ThreadLocal不是用來解決對象共享訪問問題的

線程安全:多線程環境下,用ThreadLocal去保證成員變量的安全

與其他所有的同步機制相同,ThreadLocal同樣是爲了解決多線程中的對同一變量的訪問衝突

 

ThreadLocal源碼分析:(基於JDK1.8)

構造方法:

public ThreadLocal() {
}

內部方法:(非公有)

getMap():

// 返回線程類維護的ThreadLocalMap對象
ThreadLocalMap getMap(Thread t) {  
    return t.threadLocals;
}

createMap()&& createInheritedMap():

給Thread類中的ThreadLocalMap對象進行賦值。將該類對象(ThreadLocal)作爲key,
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

initialValue()

如果get方法沒有get到數據時的默認值。可子類覆寫
protected T initialValue() {
    return null;
}

setInitialValue()

get方法返回的默認策略
private T setInitialValue() {
    T value = initialValue(); //protected方法,用戶可以重寫
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);   // 嘗試根據當前線程作爲key,找到map
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;    // 默認返回null
}

公有方法:(public): 僅有4個

set():

以當前線程爲key,存放在Thread維護的ThreadLocalMap對象中。達到每個線程不一樣的數據,達到隔離和線程安全的效果
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);  // 獲取與當前線程維護的ThreadLocalMap對象
    if (map != null)
        map.set(this, value);         // 調用ThreadLocalMap的Set方法進行保存
    else   // 當前線程的map緩存副本不存在,則創建一個ThreadLocalMap
        createMap(t, value);
}

get():

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {               // Thread維護的ThreadLocalMap有值。
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();  // 返回一個默認的值,這個值默認爲空
}

remove():

// 使用完成後,需要remove處理
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);     // 調用ThreadLocalMap對象的remove方法進行移除。
}

withInitial():

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

 

 

ThreadLocalMap源碼分析:(JDK1.8)

ThreadLocalMap: ThreadLocal的一個靜態內部類。每個線程中都有一個獨立的ThreadLocalMap副本。它所存儲的值,只能被當前線程讀取和修改。ThreadLocal類通過操作每一個線程特有的ThreadLocalMap副本,實現了變量訪問在不同線程中的隔離。因此每個線程都有獨有的變量,不會有併發錯誤

ThreadLocalMap源碼分析:

static class ThreadLocalMap {

內部類:

ThreadLocalMap中的Entry類的key就是弱引用的,弱引用會在垃圾收集器工作的時候進行回收,只要執行垃圾回收,這些對象就會被回收
/**
 * The entries in this hash map extend WeakReference, using its main ref field as the key (which is always a ThreadLocal object).  Note that null keys (i.e. entry.get() == null) mean that the key is no longer referenced, so the entry can be expunged from table.  Such entries are referred to as "stale entries" in the code that follows.
 */
static class Entry extends WeakReference<ThreadLocal<?>> { // 弱引用,避免內存泄漏的初衷
    /** The value associated with this ThreadLocal. */
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

屬性:

/*** The initial capacity -- MUST be a power of two. */
private static final int INITIAL_CAPACITY = 16;
/** The table, resized as necessary. table.length MUST always be a power of two. */
private Entry[] table;
/*** The number of entries in the table. */
private int size = 0;
/*** The next size value at which to resize. */
private int threshold; // Default to 0

構造方法:

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);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        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++;
            }
        }
    }
}

內部方法:(全私有)

setThreshold

/*** Set the resize threshold to maintain at worst a 2/3 load factor.*/
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

nextIndex

/** * Increment i modulo len.*/
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

prevIndex

/** * Decrement i modulo len. */
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

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) {
            e.value = value;
            return;
        }
// key爲空的話,value這裏的內存會被刪除掉
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
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;
    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
    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;
    }
    // If key not found, put new entry in stale slot
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

getEntry方法

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)     // 如果key爲null,表明當前ThreadLocal以及被回收,此時需要delete掉entry
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

remove方法

private void remove(ThreadLocal<?> key) {
    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)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

expungeStaleEntry方法

// 根據索引清空entry
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;   // table數量-1
    // 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) {         // 如果key爲null,則釋放entry
            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;
}

注意:使用ThreadLocal一般都是聲明在靜態變量中,如果不斷地創建ThreadLocal而沒有調用其remove方法,則會導致內存泄漏

爲了避免內存泄漏,需要在不使用ThreadLocal時remove掉

ThreadLocal應用場景:

1. 如在線程級別,維護session,維護用戶登錄信息userID(登陸時插入,多個地方獲取)

2. 數據庫的鏈接對象 Connection,可以通過ThreadLocal來做隔離避免線程安全問題

。。。。。

內存泄漏問題(ThreadLocal):

內存泄漏發生場景:

ThreadLocalMap中Entry的key使用的是ThreadLocal的弱引用,如果一個ThreadLocal沒有外部強引用,當系統執行GC時,這個ThreadLocal勢必會被回收,這樣ThreadLocalMap中就會出現一個key爲null的Entry,而這個key=null的Entry是無法訪問的,當這個線程一直沒有結束的話,那麼就會存在一條強引用鏈:Thread Ref - > Thread -> ThreadLocalMap - > Entry -> value 永遠無法回收而造成內存泄漏

ThreadLocalMap做了防護措施:

  • 首先從ThreadLocal的直接索引位置(通過ThreadLocal.threadLocalHashCode & (len-1)運算得到)獲取Entry e,如果e不爲null並且key相同則返回e
  • 如果e爲null或key不一致則向下一個位置查詢,如果下一個位置的key和當前需要查詢的key相等,則返回對應的Entry,否則,如果key值爲null,則擦除該位置的Entry,否則繼續向下一個位置查詢

在這個過程中遇到的keynullEntry都會被擦除,那麼Entry內的value也就沒有強引用鏈,自然會被回收。

set操作也有類似的思想,將key爲null的這些Entry都刪除,防止內存泄露。

 

但這個設計一來與一個前提條件,就是調用get或者set方法,但是不是所有場景都會滿足這個場景的,所以爲了避免這類的問題,可在合適的位置手動調用ThreadLocalremove刪除不需要的ThreadLocal,防止出現內存泄漏

 

避免內存泄漏的方案:

  • 將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命週期就更長,由於一直存在ThreadLocal的強引用,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱引用訪問到Entry的value值,然後remove它,防止內存泄露
  • 每次使用完ThreadLocal,都調用它的remove()方法,清除數據。

 

 

Q:爲什麼要繼承WeakReference,既然key可以被回收,但value不能被回收?

如果ThreadLocalMap的key不爲弱引用,則set時爲強引用,當ThreadLocal被置爲null時,ThreadLocal還擁有一個強引用,就是這個key。此時,除非這個map強引用被消除,否則這個ThreadLocal的key內存空間將不會被回收。即使entry能被回收,key也不會被回收。

當繼承WeekReference時,key強引用被去除後,只要有GC發生,這個key的空間都會被回收。

 

個人總結:

Thread類中有ThreadLocal.ThreadLocalMap的引用,在一個Java線程,棧中指向了堆內存中的一個ThreadLocal.ThreadLocalMap對象,此對象保存了若干個Entry,每個Entry的key(ThreadLocal實例)是弱引用,value是強引用(類似於WeakHashMap)。

 

用到弱引用的只是key,每個key都弱引用指向threadLocal,當把threadLocal實例置爲null後,沒有任何強引用指向threadLocal實例,所以threadLocal將會被gc回收,但是value卻不能被回收,因爲其還存在於ThreadLocal.ThreadLocalMap的對象的Entry之中。只有當前Thread結束之後,所有與當前線程有關的資源纔會被GC回收。所以,如果在線程池中使用ThreadLocal,由於線程會複用,而又沒有顯示的調用remove的話的確是會有可能發生內存泄露的問題。

其實在ThreadLocal.ThreadLocalMap的get或者set方法中會探測其中的key是否被回收(調用expungeStaleEntry方法),然後將其value設置爲null,這個功能幾乎和WeakHashMap中的expungeStaleEntries()方法一樣。因此value在key被gc後可能還會存活一段時間,但最終也會被回收,但是若不再調用get或者set方法時,那麼這個value就在線程存活期間無法被釋放。

其實ThreadLocal本身可以看成是沒有內存泄露問題的,通過顯示的調用remove方法即可

 

WeakReference是保證key能被回收的方案,ThreadLocalMap內部根據nullkey移除entry是保證value能被回收的方案

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章