Java中ThreadLocal無鎖化執行緒封閉實現原理

xieyuooo的專欄發表於2016-03-01

雖然現在可以說很多程式設計師會用ThreadLocal,但是我相信大多數程式設計師還不知道ThreadLocal,而使用ThreadLocal的程式設計師大多隻是知道其然而不知其所以然,因此,使用ThreadLocal的程式設計師很多時候會被它匯入到陷進中去,其實java很多高階機制系列的很多東西都是一把雙刃劍,也就是有利必有其弊,那麼我們的方法是找到利和弊的中間平衡點,最佳的方式去解決問題。

本文首先說明ThreadLocal能做什麼,然後根據功能為什麼要用它,如何使用它,最後通過內部說明講解他的坑在哪裡,使用的人應該如何避免坑。

ThreadLocal的定義和用途的概述(我的理解):

它是一個執行緒級別變數,在併發模式下是絕對安全的變數,也是執行緒封閉的一種標準用法(除了區域性變數外),即使你將它定義為static,它也是執行緒安全的。

ThreadLocal能做什麼呢?

這個一句話不好說,我們不如來看看實際專案中遇到的一些困解:當你在專案中根據一些引數呼叫進入一些方法,然後方法再呼叫方法,進而跨物件呼叫方法,很多層次,這些方法可能都會用到一些相似的引數,例如,A中需要引數a、b、c,A呼叫B後,B中需要b、c引數,而B呼叫C方法需要a、b引數,此時不得不將所有的引數全部傳遞給B,以此類推,若有很多方法的呼叫,此時的引數就會越來越繁雜,另外,當程式需要增加引數的時候,此時需要對相關的方法逐個增加引數,是的,很麻煩,相信你也遇到過,這也是在C語言物件導向過來的一些常見處理手段,不過我們簡單的處理方法是將它包裝成物件傳遞進去,通過增加物件的屬性就可以解決這個問題,不過物件通常是有意義的,所以有些時候簡單的物件包裝增加一些擴充套件不相關的屬性會使得我們class的定義變得十分的奇怪,所以在這些情況下我們在架構這類複雜的程式的時候,我們通過使用一些類似於Scope的作用域的類來處理,名稱和使用起來都會比較通用,類似web應用中會有context、session、request、page等級別的scope,而ThreadLocal也可以解決這類問題,只是他並不是很適合解決這類問題,它面對這些問題通常是初期並沒有按照scope以及物件的方式傳遞,認為不會增加引數,當增加引數時,發現要改很多地方的地方,為了不破壞程式碼的結構,也有可能引數已經太多,已經使得方法的程式碼可讀性降低,增加ThreadLocal來處理,例如,一個方法呼叫另一個方法時傳入了8個引數,通過逐層呼叫到第N個方法,傳入了其中一個引數,此時最後一個方法需要增加一個引數,第一個方法變成9個引數是自然的,但是這個時候,相關的方法都會受到牽連,使得程式碼變得臃腫不堪。

上面提及到了ThreadLocal一種亡羊補牢的用途,不過也不是特別推薦使用的方式,它還有一些類似的方式用來使用,就是在框架級別有很多動態呼叫,呼叫過程中需要滿足一些協議,雖然協議我們會盡量的通用,而很多擴充套件的引數在定義協議時是不容易考慮完全的以及版本也是隨時在升級的,但是在框架擴充套件時也需要滿足介面的通用性和向下相容,而一些擴充套件的內容我們就需要ThreadLocal來做方便簡單的支援。

簡單來說,ThreadLocal是將一些複雜的系統擴充套件變成了簡單定義,使得相關引數牽連的部分變得非常容易,以下是我們例子說明:

Spring的事務管理器中,對資料來源獲取的Connection放入了ThreadLocal中,程式執行完後由ThreadLocal中獲取connection然後做commit和rollback,使用中,要保證程式通過DataSource獲取的connection就是從spring中獲取的,為什麼要做這樣的操作呢,因為業務程式碼完全由應用程式來決定,而框架不能要求業務程式碼如何去編寫,否則就失去了框架不讓業務程式碼去管理connection的好處了,此時業務程式碼被切入後,spring不會向業務程式碼區傳入一個connection,它必須儲存在一個地方,當底層通過ibatis、spring jdbc等框架獲取同一個datasource的connection的時候,就會呼叫按照spring約定的規則去獲取,由於執行過程都是在同一個執行緒中處理,從而獲取到相同的connection,以保證commit、rollback以及業務操作過程中,使用的connection是同一個,因為只有同一個conneciton才能保證事務,否則資料庫本身也是不支援的。

其實在很多併發程式設計的應用中,ThreadLocal起著很重要的重要,它不加鎖,非常輕鬆的將執行緒封閉做得天衣無縫,又不會像區域性變數那樣每次需要從新分配空間,很多空間由於是執行緒安全,所以,可以反覆利用執行緒私有的緩衝區。

如何使用ThreadLocal?

在系統中任意一個適合的位置定義個 ThreadLocal 變數,可以定義為 public static 型別(直接new出來一個ThreadLocal物件),要向裡面放入資料就使用set(Object),要獲取資料就用get()操作,刪除元素就用remove(),其餘的方法是非 public 的方法,不推薦使用。

下面是一個簡單例子(程式碼片段1):

public class ThreadLocalTest2 {

	public final static ThreadLocal <String>TEST_THREAD_NAME_LOCAL = new ThreadLocal<String>();

	public final static ThreadLocal <String>TEST_THREAD_VALUE_LOCAL = new ThreadLocal<String>();

	public static void main(String[]args) {
		for(int i = 0 ; i < 100 ; i++) {
			final String name = "執行緒-【" + i + "】";
			final String value =  String.valueOf(i);
			new Thread() {
				public void run() {
					try {
						TEST_THREAD_NAME_LOCAL.set(name);
						TEST_THREAD_VALUE_LOCAL.set(value);
						callA();
					}finally {
						TEST_THREAD_NAME_LOCAL.remove();
						TEST_THREAD_VALUE_LOCAL.remove();
					}
				}
			}.start();
		}
	}

	public static void callA() {
		callB();
	}

	public static void callB() {
		new ThreadLocalTest2().callC();
	}

	public void callC() {
		callD();
	}

	public void callD() {
		System.out.println(TEST_THREAD_NAME_LOCAL.get() + "/t=/t" + TEST_THREAD_VALUE_LOCAL.get());
	}
}

這裡模擬了100個執行緒去訪問分別設定 name 和 value ,中間故意將 name 和 value 的值設定成一樣,看是否會存在併發的問題,通過輸出可以看出,執行緒輸出並不是按照順序輸出,說明是並行執行的,而執行緒 name 和 value 是可以對應起來的,中間通過多個方法的呼叫,以模實際的呼叫中引數不傳遞,如何獲取到對應的變數的過程,不過實際的系統中往往會跨類,這裡僅僅在一個類中模擬,其實跨類也是一樣的結果,大家可以自己去模擬就可以。

相信看到這裡,很多程式設計師都對 ThreadLocal 的原理深有興趣,看看它是如何做到的,盡然引數不傳遞,又可以像區域性變數一樣使用它,的確是蠻神奇的,其實看看就知道是一種設定方式,看到名稱應該是是和Thread相關,那麼廢話少說,來看看它的原始碼吧,既然我們用得最多的是set、get和remove,那麼就從set下手:

set(T obj)方法為(程式碼片段2):

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

首先獲取了當前的執行緒,和猜測一樣,然後有個 getMap 方法,傳入了當前執行緒,我們先可以理解這個map是和執行緒相關的map,接下來如果   不為空,就做set操作,你跟蹤進去會發現,這個和HashMap的put操作類似,也就是向map中寫入了一條資料,如果為空,則呼叫createMap方法,進去後,看看( 程式碼片段3 ):

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

返現建立了一個ThreadLocalMap,並且將傳入的引數和當前ThreadLocal作為K-V結構寫入進去( 程式碼片段4 ):

ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
	table = new Entry[INITIAL_CAPACITY];
	int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
	table[i] = new Entry(firstKey, firstValue);
	size = 1;
	setThreshold(INITIAL_CAPACITY);
}

這裡就不說明ThreadLocalMap的結構細節,只需要知道它的實現和HashMap類似,只是很多方法沒有,也沒有implements Map,因為它並不想讓你通過某些方式(例如反射)獲取到一個Map對他進一步操作,它是一個ThreadLocal裡面的一個static內部類,default型別,僅僅在java.lang下面的類可以引用到它,所以你可以想到Thread可以引用到它。

我們再回過頭來看看getMap方法,因為上面我僅僅知道獲取的Map是和執行緒相關的,而通過 程式碼片段3 ,有一個t.threadLocalMap = new ThreadLocalMap(this, firstValue)的時候,相信你應該大概有點明白,這個變數應該來自Thread裡面,我們根據getMap方法進去看看:

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

是的,是來自於Thread,而這個Thread正好又是當前執行緒,那麼進去看看定義就是:

ThreadLocal.ThreadLocalMap threadLocals = null;

這個屬性就是在Thread類中,也就是每個Thread預設都有一個ThreadLocalMap,用於存放執行緒級別的區域性變數,通常你無法為他賦值,因為這樣的賦值通常是不安全的。

好像是不是有點亂,不著急,我們回頭先摸索下思路:

1、Thread裡面有個屬性是一個類似於HashMap一樣的東西,只是它的名字叫ThreadLocalMap,這個屬性是default型別的,因此同一個package下面所有的類都可以引用到,因為是Thread的區域性變數,所以每個執行緒都有一個自己單獨的Map,相互之間是不衝突的,所以即使將ThreadLocal定義為static執行緒之間也不會衝突。

2、ThreadLocal和Thread是在同一個package下面,可以引用到這個類,可以對他做操作,此時ThreadLocal每定義一個,用this作為Key,你傳入的值作為value,而this就是你定義的ThreadLocal,所以不同的ThreadLocal變數,都使用set,相互之間的資料不會衝突,因為他們的Key是不同的,當然同一個ThreadLocal做兩次set操作後,會以最後一次為準。

3、綜上所述,線上程之間並行,ThreadLocal可以像區域性變數一樣使用,且執行緒安全,且不同的ThreadLocal變數之間的資料毫無衝突。

我們繼續看看get方法和remove方法,其實就簡單了:

public T get() {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null) {
		ThreadLocalMap.Entry e = map.getEntry(this);
		if (e != null)
			return (T)e.value;
	}
	return setInitialValue();
}

通過根據當前執行緒呼叫getMap方法,也就是呼叫了t.threadLocalMap,然後在map中查詢,注意Map中找到的是Entry,也就是K-V基本結構,因為你set寫入的僅僅有值,所以,它會設定一個e.value來返回你寫入的值,因為Key就是ThreadLocal本身。你可以看到map.getEntry也是通過this來獲取的。

同樣remove方法為:

public void remove() {
	 ThreadLocalMap m = getMap(Thread.currentThread());
	 if (m != null)
		 m.remove(this);
}

同樣根據當前執行緒獲取map,如果不為空,則remove,通過this來remove。

補充下(2013-6-29),搞忘寫有什麼坑了,這個ThreadLocal有啥坑呢,大家從前面應該可以看出來,這個ThreadLocal相關的物件是被繫結到一個Map中的,而這個Map是Thread執行緒的中的一個屬性,那麼就有一個問題是,如果你不自己remove的話或者說如果你自己的程式中不知道什麼時候去remove的話,那麼執行緒不登出,這些被set進去的資料也不會被登出。

反過來說,寫程式碼中除非你清晰的認識到這個物件應該在哪裡set,哪裡remove,如果是模糊的,很可能你的程式碼中不會走remove的位置去,或導致一些邏輯問題,另外,如果不remove的話,就要等執行緒登出,我們在很多應用伺服器中,執行緒是被複用的,因為在核心分配執行緒還是有開銷的,因此在這些應用中執行緒很難會被登出掉,那麼向ThreadLocal寫入的資料自然很不容易被登出掉,這些可能在我們使用某些開源框架的時候無意中被隱藏用到,都有可能會導致問題,最後發現OOM得時候資料竟然來自ThreadLocalMap中,還不知道這些資料是從哪裡設定進去的,所以你應當注意這個坑,可能不止一個人掉進這個坑裡去過。

相關文章