1. 程式人生 > >【搞定Java併發程式設計】第6篇:ThreadLocal詳解

【搞定Java併發程式設計】第6篇:ThreadLocal詳解

上一篇:synchronized關鍵字:https://blog.csdn.net/pcwl1206/article/details/84849400

目  錄:

1、ThreadLocal是什麼?

2、ThreadLocal使用示例

3、ThreadLocal原始碼分析

3.1、ThreadLocalMap

3.2、get()

3.3、set(T  value)

3.4、initialValue()

3.5、remove()

4、ThreadLocal為什麼會記憶體洩漏

5、總結


1、ThreadLocal是什麼?

ThreadLocal是啥?以前面試別人時就喜歡問這個,有些夥伴喜歡把它和執行緒同步機制混為一談,事實上ThreadLocal與執行緒同步無關。ThreadLocal雖然提供了一種解決多執行緒環境下成員變數的問題,但是它並不是解決多執行緒共享變數的問題。那麼ThreadLocal到底是什麼呢?

API是這樣介紹它的:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).

翻譯過來就是:

該類提供了執行緒區域性 (thread-local) 變數。這些變數不同於它們的普通對應物,因為訪問某個變數(通過其get 或 set方法)的每個執行緒都有自己的區域性變數,它獨立於變數的初始化副本。ThreadLocal例項通常是類中的 private static 欄位,它們希望將狀態與某一個執行緒(例如,使用者 ID 或事務 ID)相關聯。

所以ThreadLocal與執行緒同步機制不同,執行緒同步機制是多個執行緒共享同一個變數,而ThreadLocal是為每一個執行緒建立一個單獨的變數副本,故而每個執行緒都可以獨立地改變自己所擁有的變數副本,而不會影響其他執行緒所對應的副本

。可以說ThreadLocal為多執行緒環境下變數問題提供了另外一種解決思路。

ThreadLocal定義了四個方法:

1、get():返回此執行緒區域性變數的當前執行緒副本中的值;

2、initialValue():返回此執行緒區域性變數的當前執行緒的“初始值”;

3、remove():移除此執行緒區域性變數中當前執行緒的值;

4、set(T value):將此執行緒區域性變數的當前執行緒副本中的值設定為指定值。

除了這四個方法,ThreadLocal內部還有一個靜態內部類ThreadLocalMap,該內部類才是實現執行緒隔離機制的關鍵,get()、set()、remove()都是基於該內部類操作ThreadLocalMap提供了一種用鍵值對方式儲存每一個執行緒的變數副本的方法,key為當前ThreadLocal物件,value則是對應執行緒的變數副本

對於ThreadLocal需要注意的有兩點:

1. ThreadLocal例項本身是不儲存值,它只是提供了一個在當前執行緒中找到副本值的key;

2. 是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小夥伴會弄錯他們的關係。

下圖是Thread、ThreadLocal、ThreadLocalMap的關係:

該圖片來自: http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/

2、ThreadLocal使用示例

public class SeqCount {

	private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){
		// 實現initialValue
		public Integer initialValue(){
			return 0;
		}
	};
	
	public int nextSeq(){
		seqCount.set(seqCount.get() + 1);
		return seqCount.get();
	}
	
	public static void main(String[] args) {
		
		SeqCount seqCount = new SeqCount();
		
		SeqThread thread1 = new SeqThread(seqCount);
		SeqThread thread2 = new SeqThread(seqCount);
		SeqThread thread3 = new SeqThread(seqCount);
		SeqThread thread4 = new SeqThread(seqCount);
		
		thread1.start();
		thread2.start();
		thread3.start();
		thread4.start();
	}
}

public class SeqThread extends Thread {

	private SeqCount seqCount;

	SeqThread(SeqCount seqCount) {
		this.seqCount = seqCount;
	}

	public void run() {
		for (int i = 0; i < 3; i++) {
			System.out.println(Thread.currentThread().getName() + " seqCount :" + seqCount.nextSeq());
		}
	}
}

執行結果:

從執行結果可以看出,ThreadLocal確實是可以達到執行緒隔離機制,確保變數的安全性。這裡我們想一個問題,在上面的程式碼中ThreadLocal的initialValue()方法返回的是0,假如該方法返回的是一個物件呢,會產生什麼後果呢?例如:

A a = new A();
private static ThreadLocal<A> seqCount = new ThreadLocal<A>(){
    // 實現initialValue()
    public A initialValue() {
        return a;
    }
};

class A{
    // ....
}

3、ThreadLocal原始碼分析

ThreadLocal雖然解決了這個多執行緒變數的複雜問題,但是它的原始碼實現卻是比較簡單的。ThreadLocalMap是實現ThreadLocal的關鍵,我們先從它入手。

3.1、ThreadLocalMap

ThreadLocalMap是ThreadLocal中的一個靜態內部類。ThreadLocalMap其內部利用Entry來實現key-value的儲存,如下:

public class ThreadLocal<T> {
    
    // ... 其他方法    
    
    static class ThreadLocalMap {

        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例項)的引用為一個弱引用(關於弱引用這裡就不多說了,感興趣的可以關注這篇部落格:Java 理論與實踐: 用弱引用堵住記憶體洩漏)。

ThreadLocalMap的原始碼稍微多了點,我們就看兩個最核心的方法getEntry()、set(ThreadLocal> key, Object value)方法。

3.1.1、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回收,能夠有效地避免記憶體洩漏。

3.1.2、set(ThreadLocal> key, Object value)

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

    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;

    // 根據 ThreadLocal 的雜湊值,查詢對應元素在陣列中的位置
    int i = key.threadLocalHashCode & (len - 1);

    // 採用“線性探測法”,尋找合適位置
    for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {

        ThreadLocal<?> k = e.get();

        // key 存在,直接覆蓋
        if (k == key) {
            e.value = value;
            return;
        }

        // key == null,但是存在值(因為此處的e != null),說明之前的ThreadLocal物件已經被回收了
        if (k == null) {
            // 用新元素替換陳舊的元素
            replaceStaleEntry(key, value, i);
            return;
        }
	}

        // ThreadLocal對應的key例項不存在也沒有陳舊元素,new 一個
	tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);

	int sz = ++size;

	// cleanSomeSlots 清除陳舊的Entry(key == null)
	// 如果沒有清理陳舊的 Entry 並且陣列中的元素大於了閾值,則進行 rehash
	if (!cleanSomeSlots(i, sz) && sz >= threshold)
		rehash();
}

這個set()操作和我們在集合瞭解的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就可以看出他們的定義。

上面的兩個方法都是靜態內部類ThreadLocalMap中的方法,下面讓我們看看ThreadLocal中的其他方法。

3.2、get()

返回此執行緒區域性變數的當前執行緒副本中的值。

public T get() {
    // 獲取當前執行緒
    Thread t = Thread.currentThread();
 
    // 獲取當前執行緒的成員變數 threadLocal
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 從當前執行緒的ThreadLocalMap獲取相對應的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
 
            // 獲取目標值        
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

首先通過當前執行緒獲取所對應的成員變數ThreadLocalMap,然後通過ThreadLocalMap獲取當前ThreadLocal的Entry,最後通過所獲取的Entry獲取目標值result。

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

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

3.3、set(T  value)

將此執行緒區域性變數的當前執行緒副本中的值設定為指定值。

 public void set(T value) {
    Thread t = Thread.currentThread();   // 獲取當前執行緒
    ThreadLocalMap map = getMap(t);  // 獲取當前執行緒所對應的ThreadLocalMap
    if (map != null)
        map.set(this, value);  // ThreadLocalMap不為空時,呼叫ThreadLocalMap的set()方法
    else
        createMap(t, value);   // ThreadLocalMap為空時,呼叫createMap()方法新建一個ThreadLocalMap
}

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

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

3.4、initialValue()

返回此執行緒區域性變數的當前執行緒的“初始值”;

protected T initialValue() {
    return null;
}

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

3.5、remove()

將當前執行緒區域性變數的值移除。

public void remove() {
    // 獲取當前執行緒的ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread()); 
    if (m != null)
        m.remove(this);  // 如果當前執行緒的ThreadLocalMap不為空,則移除
    }

4、ThreadLocal為什麼會記憶體洩漏

先看這樣一個小案例:

在JAVA裡面,存在強引用、弱引用、軟引用、虛引用。這裡主要談一下強引用和弱引用。

  • 強引用類似於:

A a = new A();

B b = new B(); 

  • 現在考慮這種情況:

C c = new C(b);

b = null;

考慮下GC的情況。要知道b被置為null,那麼是否意味著一段時間後GC工作可以回收b所分配的記憶體空間呢?

答案是否定的,因為即便b被置為null,但是c仍然持有對b的引用,而且還是強引用,所以GC不會回收b原先所分配的空間!既不能回收利用,又不能使用,這就造成了記憶體洩露

那麼如何處理呢?

可以使c = null;也可以使用弱引用!(WeakReference w = new WeakReference(b);)

下面就來看看ThreadLocal的記憶體洩露是怎麼一回事:

前面提到每個Thread都有一個ThreadLocal.ThreadLocalMap的map,該map的key為ThreadLocal例項,它為一個弱引用,我們知道弱引用有利於GC回收。當ThreadLocal的key == null時,GC就會回收這部分空間,但是value卻不一定能夠被回收,因為他還與Current Thread存在一個強引用關係,如下圖所示:

由於存在這個強引用關係,會導致value無法回收。如果這個執行緒物件不會銷燬,那麼這個強引用關係則會一直存在,就會出現記憶體洩漏情況。所以說只要這個執行緒物件能夠及時被GC回收,就不會出現記憶體洩漏。如果碰到執行緒池,那就更坑了。

那麼要怎麼避免這個問題呢?

在前面提過,在ThreadLocalMap中的setEntry()、getEntry(),如果遇到key == null的情況,會對value設定為null。當然我們也可以顯示呼叫ThreadLocal的remove()方法進行處理


5、總結

下面對ThreadLocal進行簡單的總結:

1、ThreadLocal 不是用於解決共享變數的問題的,也不是為了協調執行緒同步而存在,而是為了方便每個執行緒處理自己的狀態而引入的一個機制。這點至關重要;

2、每個Thread內部都有一個ThreadLocal.ThreadLocalMap型別的成員變數,該成員變數用來儲存實際的ThreadLocal變數副本;

3、ThreadLocal並不是為執行緒儲存物件的副本,它僅僅只起到一個索引的作用。它的主要目的是為每一個執行緒隔離一個類的例項,這個例項的作用範圍僅限於執行緒內部。

本文轉發自:http://cmsblogs.com/?p=2442

上一篇:synchronized關鍵字:https://blog.csdn.net/pcwl1206/article/details/84849400