1. 程式人生 > >Java併發程式設計之ThreadLocal詳解

Java併發程式設計之ThreadLocal詳解

ThreadLocal是什麼?

  ThreadLocal是一個關於建立執行緒區域性變數的類。

  通常情況下,我們建立的變數是可以被任何一個執行緒訪問並修改的。而使用ThreadLocal建立的變數只能被當前執行緒訪問,其他執行緒則無法訪問和修改。

ThreadLocal使用示例

示例1:ThreadLocal宣告基本型別變數

示例1 程式碼段(1)

示例1 程式碼段(2)

  執行程式,可以得到:

示例1 執行結果

  從執行結果可以看出,對於基本型別變數,ThreadLocal確實是可以達到執行緒隔離作用的。

示例2:ThreadLocal宣告自定義型別的物件

示例2 自定義型別

示例2 程式碼段(1)

示例2 程式碼段(2)

  執行程式,可以得到:

示例2 執行結果

  從執行結果可以看出,對於自定義型別的物件,ThreadLocal也是可以達到執行緒隔離作用的。

示例3:ThreadLocal宣告的變數都指向同一個物件

示例3 對程式程式碼稍作修改

  對示例2的程式碼稍作修改,使得ThreadLocal宣告的變數初始化時不再例項化一個新的物件,而是讓它指向同一個物件,執行檢視結果:

示例3 執行結果

  很顯然,在這裡,並沒有通過ThreadLocal達到執行緒隔離的機制,可是ThreadLocal不是保證執行緒安全的麼?這是什麼鬼? 顯然,雖說ThreadLocal讓訪問某個變數的執行緒都擁有自己的區域性變數,但是如果這個區域性變數都指向同一個物件的話,這個時候,ThreadLocal就失效了。

ThreadLocal原始碼剖析

  ThreadLocal類的原始碼在java.lang包中。其中主要有四個方法:

1. get()

// 返回當前執行緒所對應的執行緒變數
public T get() {
    // 獲取當前執行緒
    Thread t = Thread.currentThread();
    // 獲取當前執行緒的成員變數 threadLocal
    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();
}

  從原始碼中可以看到,get()方法首先通過當前執行緒獲取所對應的成員變數ThreadLocalMap,然後通過ThreadLocalMap獲取當前ThreadLocal的鍵值對Entry,最後通過該Entry獲取目標值result。

  其中,getMap()方法可以獲取當前執行緒所對應的ThreadLocalMap,其原始碼如下:

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

2. set(T value)

// 設定當前執行緒的執行緒區域性變數的值。
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

  set方法首先獲取當前執行緒所對應的ThreadLocalMap,如果不為空,則呼叫ThreadLocalMap的set()方法,key就是當前ThreadLocal,如果不存在,則呼叫createMap()方法新建一個,其原始碼如下:

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

3. initialValue()

// 返回該執行緒區域性變數的初始值。
protected T initialValue() {
    return null;
}

  該方法定義為protected級別且返回為null,很明顯是要子類重寫來實現它的,所以我們在使用ThreadLocal的時候一般都應該覆蓋該方法。該方法不能顯示呼叫,只有在第一次呼叫get()或者set()方法時才會被執行,並且僅執行1次。

4. remove()

// 將當前執行緒區域性變數的值刪除
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

  該方法的目的是減少記憶體佔用,避免出現因為執行緒遲遲未結束而導致記憶體洩漏的情況。需要指出的是,當執行緒結束後,對應該執行緒的區域性變數將自動被垃圾回收,所以顯式呼叫該方法清除執行緒的區域性變數並不是必須的操作,但它可以加快記憶體回收的速度。

ThreadLocalMap類

  從ThreadLocal的原始碼中我們可以看到,ThreadLocal的實現比較簡單,主要是依賴於ThreadLocalMap這個類,我們有必要好好理解一下後者。

  根據命名就可以看出,ThreadLocalMap,它實際上是一個Map鍵值對。在其內部使用了Entry的方式來實現key-value的儲存:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

  在上面的程式碼中,Entry內的Key就是ThreadLocal,而Value就是執行緒私有的那個變數。同時,Entry也繼承WeakReference,所以說Entry所對應key(ThreadLocal例項)的引用是一個弱引用。

  下面來看一下ThreadLocalMap類中幾個核心的方法:

1. set()

    private void set(ThreadLocal<?> key, Object value) {

        // We don't use a fast path as with get() because it is at
        // least as common to use set() to create new entries as
        // it is to replace existing ones, in which case, a fast
        // path would fail more often than not.

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

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

        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

  原始碼的意思簡單明瞭,根據要儲存的key到Entry陣列中去匹配,如果key已經存在就更新值,否則建立新的entry寫入。

  值得注意的是,這裡的set()操作和我們在集合Map瞭解的put()方式有點兒不一樣,雖然他們都是key-value結構,不同點在於他們解決雜湊衝突的方式不同。 集合Map的put()採用的是拉鍊法,即在每個陣列元素的位置,存入連結串列來解決衝突。而ThreadLocalMap的set()則是採用開放定址法來解決衝突的。

  set()操作除了儲存元素外,還有一個很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),這兩個方法可以清除掉key == null 的例項,防止記憶體洩漏。在set()方法中還有一個變數很重要:threadLocalHashCode,定義如下:

private final int threadLocalHashCode = nextHashCode();

  從名字上面我們可以看出threadLocalHashCode應該是ThreadLocal的雜湊值,定義為final,表示ThreadLocal一旦建立其雜湊值就已經確定了,生成過程則是呼叫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例項的threadLocalHashCode的值,HASH_INCREMENT則表示分配兩個ThradLocal例項的threadLocalHashCode的增量,從nextHashCode就可以看出他們的定義。

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

  由於採用了開放定址法,所以當前key的雜湊值和元素在陣列的索引並不是完全對應的,首先取一個探測數(key的雜湊值),如果所對應的key就是我們要找的元素,則返回,否則呼叫getEntryAfterMiss()再尋找,原始碼如下:

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)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

  這裡有一個重要的地方,當key == null時,呼叫了expungeStaleEntry()方法,該方法用於處理key == null,有利於GC回收,能夠有效地避免記憶體洩漏。

ThreadLocal與記憶體洩漏

ThreadLocal實現原理

圖:ThreadLocal實現原理

  前面提到過,每個Thread都有一個ThreadLocal.ThreadLocalMap,該map的key為ThreadLocal例項的一個弱引用,我們知道弱引用有利於GC回收。當ThreadLocal的key == null時,GC就會回收這部分空間,但是value卻不一定能夠被回收。

  如果當前執行緒再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠無法回收,造成記憶體洩漏。

  其實,ThreadLocal類的設計中已經考慮到這種情況,也加上了一些防護措施:在觸發ThreadLocal的remove()時會清除執行緒ThreadLocalMap裡key為null的value。