首頁 > 軟體

深入理解Java並行程式設計之ThreadLocal

2022-08-01 14:00:24

ThreadLocal簡介

變數值的共用可以使用public static的形式,所有執行緒都使用同一個變數,如果想實現每一個執行緒都有自己的共用變數該如何實現呢?JDK中的ThreadLocal類正是為了解決這樣的問題。

ThreadLocal類並不是用來解決多執行緒環境下的共用變數問題,而是用來提供執行緒內部的共用變數,在多執行緒環境下,可以保證各個執行緒之間的變數互相隔離、相互獨立。線上程中,可以通過get()/set()方法來存取變數。ThreadLocal範例通常來說都是private static型別的,它們希望將狀態與執行緒進行關聯。這種變數線上程的生命週期內起作用,可以減少同一個執行緒內多個函數或者元件之間一些公共變數的傳遞的複雜度。

我們先通過一個例子來看一下ThreadLocal的基本用法:

public class ThreadLocalTest {
	static class MyThread extends Thread {
		private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
		
		@Override
		public void run() {
			super.run();
			for (int i = 0; i < 3; i++) {
				threadLocal.set(i);
				System.out.println(getName() + " threadLocal.get() = " + threadLocal.get());
			}
		}
	}
	
	public static void main(String[] args) {
		MyThread myThreadA = new MyThread();
		myThreadA.setName("ThreadA");
		
		MyThread myThreadB = new MyThread();
		myThreadB.setName("ThreadB");
		
		myThreadA.start();
		myThreadB.start();
	}
}

執行結果(不唯一):

ThreadA threadLocal.get() = 0
ThreadB threadLocal.get() = 0
ThreadA threadLocal.get() = 1
ThreadA threadLocal.get() = 2
ThreadB threadLocal.get() = 1
ThreadB threadLocal.get() = 2

雖然兩個執行緒都在向threadLocal物件中set()資料值,但每個執行緒都還是能取出自己設定的資料,確實可以達到隔離執行緒變數的效果。

ThreadLocal原始碼解析

ThreadLocal常用方法介紹

  • get()方法:獲取與當前執行緒關聯的ThreadLocal值。
  • set(T value)方法:設定與當前執行緒關聯的ThreadLocal值。
  • initialValue()方法:設定與當前執行緒關聯的ThreadLocal初始值。

當呼叫get()方法的時候,若是與當前執行緒關聯的ThreadLocal值已經被設定過,則不會呼叫initialValue()方法;否則,會呼叫initialValue()方法來進行初始值的設定。通常initialValue()方法只會被呼叫一次,除非呼叫了remove()方法之後又呼叫get()方法,此時,與當前執行緒關聯的ThreadLocal值處於沒有設定過的狀態(其狀態體現在原始碼中,就是執行緒的ThreadLocalMap物件是否為null),initialValue()方法仍會被呼叫。

initialValue()方法是protected型別的,很顯然是建議在子類過載該函數的,所以通常該方法都會以匿名內部類的形式被過載,以指定初始值,例如:

public class ThreadLocalTest {
	public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
		@Override
		protected Integer initialValue() {
			return Integer.valueOf(1);
		}
	};
}

remove()方法:將與當前執行緒關聯的ThreadLocal值刪除。

實現原理

ThreadLocal最簡單的實現方式就是ThreadLocal類內部有一個執行緒安全的Map,然後用執行緒的ID作為Map的key,範例物件作為Map的value,這樣就能達到各個執行緒的值隔離的效果。

JDK最早期的ThreadLocal就是這樣設計的,但是,之後ThreadLocal的設計換了一種方式,我們先看get()方法的原始碼,然後進一步介紹ThreadLocal的實現方式:

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

get()方法主要做了以下事情:

1、呼叫Thread.currentThread()獲取當前執行緒物件t;

2、根據當前執行緒物件,呼叫getMap(Thread)獲取執行緒對應的ThreadLocalMap物件:

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

threadLocals是Thread類的成員變數,初始化為null:

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

3、如果獲取的map不為空,則在map中以ThreadLocal的參照作為key來在map中獲取對應的value e,否則轉到步驟5;

4、若e不為null,則返回e中儲存的value值,否則轉到步驟5;

5、呼叫setInitialValue()方法,對執行緒的ThreadLocalMap物件進行初始化操作,ThreadLocalMap物件的key為ThreadLocal物件,value為initialValue()方法的返回值。

從上面的分析中,可以看到,ThreadLocal的實現離不開ThreadLocalMap類,ThreadLocalMap類是ThreadLocal的靜態內部類。每個Thread維護一個ThreadLocalMap對映表,這個對映表的key是ThreadLocal範例本身,value是真正需要儲存的Object。這樣的設計主要有以下幾點優勢:

  • 這樣設計之後每個Map的Entry數量變小了:之前是Thread的數量,現在是ThreadLocal的數量,能提高效能;
  • 當Thread銷燬之後對應的ThreadLocalMap也就隨之銷燬了,能減少記憶體使用量。

ThreadLocalMap原始碼分析

ThreadLocalMap是用來儲存與執行緒關聯的value的雜湊表,它具有HashMap的部分特性,比如容量、擴容閾值等,它內部通過Entry類來儲存key和value,Entry類的定義為:

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

Entry繼承自WeakReference,通過上述原始碼super(k);可以知道,ThreadLocalMap是使用ThreadLocal的弱參照作為Key的。

分析到這裡,我們可以得到下面這個物件之間的參照結構圖(其中,實線為強參照,虛線為弱參照):

我們知道,弱參照物件在Java虛擬機器器進行垃圾回收時,就會被釋放,那我們考慮這樣一個問題:

ThreadLocalMap使用ThreadLocal的弱參照作為key,如果一個ThreadLocal沒有外部關聯的強參照,那麼在虛擬機器器進行垃圾回收時,這個ThreadLocal會被回收,這樣,ThreadLocalMap中就會出現key為null的Entry,這些key對應的value也就再無妨存取,但是value卻存在一條從Current Thread過來的強參照鏈。因此只有當Current Thread銷燬時,value才能得到釋放。

該強參照鏈如下:

CurrentThread Ref -> Thread -> ThreadLocalMap -> Entry -> value

因此,只要這個執行緒物件被gc回收,那些key為null對應的value也會被回收,這樣也沒什麼問題,但線上程物件不被回收的情況下,比如使用執行緒池的時候,核心執行緒是一直在執行的,執行緒物件不會回收,若是在這樣的執行緒中存在上述現象,就可能出現記憶體洩露的問題。

那在ThreadLocalMap中是如何解決這個問題的呢?

在獲取key對應的value時,會呼叫ThreadLocalMap的getEntry(ThreadLocal<?> key)方法,該方法原始碼如下:

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.threadLocalHashCode & (table.length - 1)來計算儲存key的Entry的索引位置,然後判斷對應的key是否存在,若存在,則返回其對應的value,否則,呼叫getEntryAfterMiss(ThreadLocal<?>, int, Entry)方法,原始碼如下:

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

ThreadLocalMap採用線性探查的方式來處理雜湊衝突,所以會有一個while迴圈去查詢對應的key,在查詢過程中,若發現key為null,即通過弱參照的key被回收了,會呼叫expungeStaleEntry(int)方法,其原始碼如下:

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
 
    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;
 
    // 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) {
            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;
}

通過上述程式碼可以發現,若key為null,則該方法通過下述程式碼來清理與key對應的value以及Entry:

// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;

此時,CurrentThread Ref不存在一條到Entry物件的強參照鏈,Entry到value物件也不存在強參照,那在程式執行期間,它們自然也就會被回收。expungeStaleEntry(int)方法的後續程式碼就是以線性探查的方式,調整後續Entry的位置,同時檢查key的有效性。

在ThreadLocalMap中的set()/getEntry()方法中,都會呼叫expungeStaleEntry(int)方法,但是如果我們既不需要新增value,也不需要獲取value,那還是有可能產生記憶體漏失的。所以很多情況下需要使用者手動呼叫ThreadLocal的remove()函數,手動刪除不再需要的ThreadLocal,防止記憶體洩露。若對應的key存在,remove()方法也會呼叫expungeStaleEntry(int)方法,來刪除對應的Entry和value。

其實,最好的方式就是將ThreadLocal變數定義成private static的,這樣的話ThreadLocal的生命週期就更長,由於一直存在ThreadLocal的強參照,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱參照存取到Entry的value值,然後remove它,可以防止記憶體洩露。

InheritableThreadLocal

InheritableThreadLocal繼承自ThreadLocal,使用InheritableThreadLocal類可以使子執行緒繼承父執行緒的值,來看一段範例程式碼:

public class ThreadLocalTest {
	private static InheritableThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>() {
		@Override
		protected Integer initialValue() {
			return Integer.valueOf(10);
		}
	};
	
	static class MyThread extends Thread {
		@Override
		public void run() {
			super.run();
			System.out.println(getName() + " inheritableThreadLocal.get() = " + inheritableThreadLocal.get());
		}
	}
	
	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName() + " inheritableThreadLocal.get() = " + inheritableThreadLocal.get());
		
		MyThread myThread = new MyThread();
		myThread.setName("執行緒A");
		myThread.start();
	}
}

執行結果:

main inheritableThreadLocal.get() = 10

執行緒A inheritableThreadLocal.get() = 10

可以看到子執行緒成功繼承了父執行緒的值。

父執行緒還可以設定子執行緒的初始值,只需要重寫InheritableThreadLocal類的childValue(T)方法即可,將上述程式碼的inheritableThreadLocal 定義修改為如下方式:

private static InheritableThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {
        return Integer.valueOf(10);
    }
    
    @Override
    protected Integer childValue(Integer parentValue) {
        return Integer.valueOf(5);
    }
};

執行結果為:

main inheritableThreadLocal.get() = 10

執行緒A inheritableThreadLocal.get() = 5

可以看到,子程序成功獲取到了父程序設定的初始值。

使用InheritableThreadLocal類需要注意的一點是,如果子執行緒在取得值的同時,主執行緒將InheritableThreadLocal中的值進行更改,那子執行緒獲取的還是舊值。

執行緒中用來實現上述功能的ThreadLocalMap類變數為

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

InheritableThreadLocal類的實現很簡單,主要是重寫了ThreadLocal類的getMap(Thread)方法和createMap(Thread, T)方法,將其中操作的ThreadLocalMap變數修改為了inheritableThreadLocals,這裡不再進一步敘述。

參考資料

高洪巖:《Java多執行緒程式設計核心技術

ThreadLocal和synchronized的區別

到此這篇關於深入理解Java並行程式設計之ThreadLocal 的文章就介紹到這了,更多相關Java ThreadLocal 內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com