1. 程式人生 > >ThreadLocal實現原理和記憶體洩漏問題

ThreadLocal實現原理和記憶體洩漏問題

1.概述

ThreadLocal不是為了解決多執行緒訪問共享變數,而是為每個執行緒建立一個單獨的變數副本,變數在多執行緒環境下訪問(通過get或set方法訪問)時能保證各個執行緒裡的變數相對獨立於其他執行緒內的變數,ThreadLocal例項通常來說都是private static型別。

2.實現原理

ThreadLocal可以看做是一個容器,容器裡面存放著屬於當前執行緒的變數。ThreadLocal類提供了四個對外開放的介面方法,這也是使用者操作ThreadLocal類的基本方法:
(1) void set(Object value)設定當前執行緒的執行緒區域性變數的值。
(2) public Object get()該方法返回當前執行緒所對應的執行緒區域性變數。
(3) public void remove()將當前執行緒區域性變數的值刪除,目的是為了減少記憶體的佔用

,該方法是JDK 5.0新增的方法。需要指出的是,當執行緒結束後,對應該執行緒的區域性變數將自動被垃圾回收,所以顯式呼叫該方法清除執行緒的區域性變數並不是必須的操作,但它可以加快記憶體回收的速度
(4) protected Object initialValue()返回該執行緒區域性變數的初始值,該方法是一個protected的方法,顯然是為了讓子類覆蓋而設計的。這個方法是一個延遲呼叫方法,線上程第1次呼叫get()或set(Object)時才執行,並且僅執行1次,ThreadLocal中的預設實現直接返回一個null。

下面我們來看原始碼,首先,在Thread類中有一行:

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

其中ThreadLocalMap類的定義是在ThreadLocal類中,真正的引用卻是在Thread類中。同時,ThreadLocalMap中用於儲存資料的entry定義:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }

我們可以發現這個Map的key是ThreadLocal類的例項物件,value為使用者的值。ThreadLocal的set和get方法程式碼:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

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();
}

其中的getMap方法:

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

給當前Thread類物件初始化ThreadlocalMap屬性:

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

到這裡,我們就可以理解ThreadLocal究竟是如何工作的了。

1.Thread類中有一個成員變數屬於ThreadLocalMap類(一個定義在ThreadLocal類中的內部類),它是一個Map,他的key是ThreadLocal例項物件。
2.當為ThreadLocal類的物件set值時,首先獲得當前執行緒的ThreadLocalMap類屬性,然後以ThreadLocal類的物件為key,設定value。get值時則類似。
3.ThreadLocal變數的活動範圍為某執行緒,是該執行緒“專有的,獨自霸佔”的,對該變數的所有操作均由該執行緒完成!也就是說,ThreadLocal 不是用來解決共享物件的多執行緒訪問的競爭問題的,因為ThreadLocal.set() 到執行緒中的物件是該執行緒自己使用的物件,其他執行緒是不需要訪問的,也訪問不到的。當執行緒終止後,這些值會作為垃圾回收。
4.每個執行緒獨自擁有一個變數,並非是共享的。

ThreadLocal的作用是提供執行緒內的區域性變數,這種變數在執行緒的生命週期內起作用。作用:提供一個執行緒內公共變數(比如本次請求的使用者資訊),減少同一個執行緒內多個函式或者元件之間一些公共變數的傳遞的複雜度,或者為執行緒提供一個私有的變數副本,這樣每一個執行緒都可以隨意修改自己的變數副本,而不會對其他執行緒產生影響。

如何實現一個執行緒多個ThreadLocal物件,每一個ThreadLocal物件是如何區分的呢?
檢視原始碼,可以看到:

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);
}

對於每一個ThreadLocal物件,都有一個final修飾的int型的threadLocalHashCode不可變屬性,對於基本資料型別,可以認為它在初始化後就不可以進行修改,所以可以唯一確定一個ThreadLocal物件。

但是如何保證兩個同時例項化的ThreadLocal物件有不同的threadLocalHashCode屬性:在ThreadLocal類中,還包含了一個static修飾的AtomicInteger(提供原子操作的Integer類)成員變數(即類變數)和一個static final修飾的常量(作為兩個相鄰nextHashCode的差值)。由於nextHashCode是類變數,所以每一次呼叫ThreadLocal類都可以保證nextHashCode被更新到新的值,並且下一次呼叫ThreadLocal類這個被更新的值仍然可用,同時AtomicInteger保證了nextHashCode自增的原子性。

為什麼不直接用執行緒id來作為ThreadLocalMap的key?
這一點很容易理解,因為直接用執行緒id來作為ThreadLocalMap的key,無法區分放入ThreadLocalMap中的多個value。比如我們放入了兩個字串,你如何知道我要取出來的是哪一個字串呢?
而使用ThreadLocal作為key就不一樣了,由於每一個ThreadLocal物件都可以由threadLocalHashCode屬性唯一區分或者說每一個ThreadLocal物件都可以由這個物件的名字唯一區分,所以可以用不同的ThreadLocal作為key,區分不同的value,方便存取。

3.ThreadLocal的記憶體洩露問題

根據上面Entry方法的原始碼,我們知道ThreadLocalMap是使用ThreadLocal的弱引用作為Key的。下圖是本文介紹到的一些物件之間的引用關係圖,實線表示強引用,虛線表示弱引用:

這裡寫圖片描述

 如上圖,ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用引用他,那麼系統gc的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當前執行緒再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鏈:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永遠無法回收,造成記憶體洩露。
  
ThreadLocalMap設計時的對上面問題的對策:ThreadLocalMap的getEntry函式的流程大概為:
1.首先從ThreadLocal的直接索引位置(通過ThreadLocal.threadLocalHashCode & (table.length-1)運算得到)獲取Entry e,如果e不為null並且key相同則返回e;
2.如果e為null或者key不一致則向下一個位置查詢,如果下一個位置的key和當前需要查詢的key相等,則返回對應的Entry。否則,如果key值為null,則擦除該位置的Entry,並繼續向下一個位置查詢。在這個過程中遇到的key為null的Entry都會被擦除,那麼Entry內的value也就沒有強引用鏈,自然會被回收。仔細研究程式碼可以發現,set操作也有類似的思想,將key為null的這些Entry都刪除,防止記憶體洩露。

但是光這樣還是不夠的,上面的設計思路依賴一個前提條件:要呼叫ThreadLocalMap的getEntry函式或者set函式。這當然是不可能任何情況都成立的,所以很多情況下需要使用者手動呼叫ThreadLocal的remove函式,手動刪除不再需要的ThreadLocal,防止記憶體洩露。所以JDK建議將ThreadLocal變數定義成private static的,這樣的話ThreadLocal的生命週期就更長,由於一直存在ThreadLocal的強引用,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱引用訪問到Entry的value值,然後remove它,防止記憶體洩露。

關於ThreadLocalMap內部類的簡單介紹:
初始容量16,負載因子2/3,解決衝突的方法是再hash法,也就是:在當前hash的基礎上再自增一個常量。