ThreadLocal的簡單理解

huan1993發表於2022-06-28

一、背景

最近有人問我ThreadLocal是如何做到在每個執行緒中的值都是隔離的,此處寫篇文章來簡單記錄下。

二、ThreadLocal解決的問題

  1. 該資料屬於該執行緒Thread自身,別的執行緒無法對其影響。(需要注意:需要呼叫ThreadLocal的remove方法)
  2. 不存線上程安全問題。(因為ThreadLocal型別的變數只有自身的執行緒可以訪問,所以這點是成立的。)

比如:

使用者登入成功後,需要將登入使用者資訊儲存起來,以方便在系統中的任何地方都可以使用到,那麼此時就可以使用ThreadLocal來實現。例如:Spring Security中的ThreadLocalSecurityContextHolderStrategy類。

三、如何建立一個ThreadLocal例項

private static final ThreadLocal<String> USER_NAME = new ThreadLocal<>();

ThreadLocal的例項推薦使用private static final來修飾。

四、ThreadLocal如何做到執行緒變數隔離

1、理解3個類

  1. ThreadLocal: 此類提供了一個簡單的set,get,remove方法,用於設定,獲取或移除 繫結到執行緒本地變數中的值。

  2. ThreadLocalMap: 這是在ThreadLocal中定義的一個類,可以簡單的將它理解成一個Map,不過它的key是WeakReference弱引用型別,這樣當這個值沒有在別的地方引用時,在發生垃圾回收時,這個map的key會被自動回收,不過它的值不會被自動回收。

    static class Entry extends WeakReference<ThreadLocal<?>> {
    	Object value;
    	Entry(ThreadLocal<?> k, Object v) {
    		// key 弱引用
    		super(k);
    		// 值強引用
    		value = v;
    	}
    }
    
  3. Thread:這個是執行緒類,在這個類中存在一個threadLocals變數,具體的型別是ThreadLocal.ThreadLocalMap

2、看下set方法是如何實現的

public void set(T value) {
	// 獲取當前執行緒
	Thread t = Thread.currentThread();
	// 獲取繫結到這個執行緒自身的 ThreadLocalMap,這個ThreadLocalMap是從Thread類的`threadLocals`變數中獲取的
	ThreadLocalMap map = getMap(t);
	if (map != null) {
		// 向map中設定值,key為 ThreadLocal 物件的例項。
		map.set(this, value);
	} else {
		// 如果map不存在,則建立出來。
		createMap(t, value);
	}
}

通過上方的程式碼,我們可知: 當我們向ThreadLocal中設定一個值,會經過如下幾個步驟:

  1. 獲取當前執行緒Thread
  2. 獲取當前執行緒的 ThreadLocalMap 物件。
  3. ThreadLocalMap中設定值,key為ThreadLocal物件,值為具體的值。

3、看看 get 方法如何實現

public T get() {
	// 獲取當前執行緒
	Thread t = Thread.currentThread();
	// 獲取這個執行緒自身繫結的 ThreadLocalMap 物件
	ThreadLocalMap map = getMap(t);
	if (map != null) {
		// this是ThreadLocal物件,獲取Map中的Entry物件
		ThreadLocalMap.Entry e = map.getEntry(this);
		if (e != null) {
			@SuppressWarnings("unchecked")
			// 獲取具體的值
			T result = (T)e.value;
			return result;
		}
	}
	// 設定初始值
	return setInitialValue();
}

從上方的get 和 set 方法可以得知,通過往ThreadLocal物件中設定值或獲取值,其實是最終操作到Thread物件中的threadLocals欄位中,而這個欄位是Thread自身的,因此做到了隔離。

五、ThreadLocalMap中的hash衝突是如何處理的

1、ThreadLocal物件的hash值是怎樣的

private final int threadLocalHashCode = nextHashCode();
	// 該 ThreadLocal 物件自身的hash code值
    private final int threadLocalHashCode = nextHashCode();
	// 從0開始
    private static AtomicInteger nextHashCode = new AtomicInteger();
	// 每次遞增固定的值
    private static final int HASH_INCREMENT = 0x61c88647;
	// hash code 值計算
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

從上方的程式碼中可以,ThreadLocal類在例項化出來之後,它的hash code值(threadLocalHashCode)就是固定的,即使ThreadLocal呼叫了set方法,設定了別的值,它的hash code值也不會發生變化。

此欄位threadLocalHashCodeThreadLocal物件的hash值,在ThreadLocalMap中需要用到這個hash值。

2、解決hash衝突

ThreadLocalMap hash衝突

ThreadLocalMap解決hash衝突的辦法很簡單。就是通過線性探測法。如果發生了衝突,就去找陣列後面的可用位置。具體看上圖。演示的是A和B 2個ThreadLocal物件,然後發生了衝突,A和B存在的位置在那個地方。

六、ThreadLocal記憶體洩漏

ThreadLocal為什麼會存在記憶體洩漏呢?

這是因為ThreadLocalMap中的keyWeakReference型別,也就是弱引用型別,而弱引用型別的資料在沒有外部強引用型別的話,在發生gc的時候,會自動被回收掉。注意: 此時是key被回收了,但是value是沒有回收的。因此在ThreadLocalMap中的Entry[]中可能存在keynull,但是value是具體的值的物件,因此就發生了記憶體洩漏。

解決記憶體洩漏:

當我們使用完ThreadLocal物件後,需要在適當的時機呼叫ThreadLocal#remove()方法。 否則就只有等Thread自動退出才能清除,如果是使用了執行緒池,Thread會重用,清除的機會就更難。

相關文章