天天看點

ThreadLocal

雖然ThreadLocal與并發問題相關,但是許多程式員僅僅将它作為一種用于“友善傳參”的工具,胖哥認為這也許并不是ThreadLocal設計的目的,它本身是為線程安全和某些特定場景的問題而設計的。

ThreadLocal是什麼呢!

每個ThreadLocal可以放一個線程級别的變量,但是它本身可以被多個線程共享使用,而且又可以達到線程安全的目的,且絕對線程安全。

例如:

[java] view plain copy print?

  1. public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();  
public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();
      

RESOURCE代表一個可以存放String類型的ThreadLocal對象,此時任何一個線程可以并發通路這個變量,對它進行寫入、讀取操作,都是線程安全的。比如一個線程通過RESOURCE.set(“aaaa”);将資料寫入ThreadLocal中,在任何一個地方,都可以通過RESOURCE.get();将值擷取出來。

但是它也并不完美,有許多缺陷,就像大家依賴于它來做參數傳遞一樣,接下來我們就來分析它的一些不好的地方。

為什麼有些時候會将ThreadLocal作為友善傳遞參數的方式呢?例如當許多方法互相調用時,最初的設計可能沒有想太多,有多少個參數就傳遞多少個變量,那麼整個參數傳遞的過程就是零散的。進一步思考:若A方法調用B方法傳遞了8個參數,B方法接下來調用C方法->D方法->E方法->F方法等隻需要5個參數,此時在設計API時就涉及5個參數的入口,這些方法在業務發展的過程中被許多地方所複用。

某一天,我們發現F方法需要加一個參數,這個參數在A方法的入口參數中有,此時,如果要改中間方法牽涉面會很大,而且不知道修改後會不會有Bug。作為程式員的我們可能會随性一想,ThreadLocal反正是全局的,就放這裡吧,确實好解決。

但是此時你會發現系統中這種方式有點像在貼更新檔,越貼越多,我們必須要求調用相關的代碼都使用ThreadLocal傳遞這個參數,有可能會搞得亂七八糟的。換句話說,并不是不讓用,而是我們要明确它的入口和出口是可控的。

詭異的ThreadLocal最難琢磨的是“作用域”,尤其是在代碼設計之初很亂的情況下,如果再增加許多ThreadLocal,系統就會逐漸變成神龍見首不見尾的情況。有了這樣一個省事的東西,可能許多小夥伴更加不在意設計,因為大家都認為這些問題都可以通過變化的手段來解決。胖哥認為這是一種惡性循環。

對于這類業務場景,應當提前有所準備,需要粗粒度化業務模型,即使要用ThreadLocal,也不是加一個參數就加一個ThreadLocal變量。例如,我們可以設計幾種對象來封裝入口參數,在接口設計時入口參數都以對象為基礎。

也許一個類無法表達所有的參數意思,而且那樣容易導緻強耦合。

通常我們按照業務模型分解為幾大類型對象作為它們的參數包裝,并且将按照對象屬性共享情況進行抽象,在繼承關系的每一個層次各自擴充相應的參數,或者說加參數就在對象中加,共享參數就在父類中定義,這樣的參數就逐漸規範化了。

我們回到正題,探讨一下ThreadLocal到底是用來做什麼的?為此我們探讨下文中的幾個話題。

(1)應用場景及使用方式

為了說明ThreadLocal的應用場景,我們來看一個架構的例子。Spring的事務管理器通過AOP切入業務代碼,在進入業務代碼前,會根據對應的事務管理器提取出相應的事務對象,假如事務管理器是DataSourceTransactionManager,就會從DataSource中擷取一個連接配接對象,通過一定的包裝後将其儲存在ThreadLocal中。并且Spring也将DataSource進行了包裝,重寫了其中的getConnection()方法,或者說該方法的傳回将由Spring來控制,這樣Spring就能讓線程内多次擷取到的Connection對象是同一個。

為什麼要放在ThreadLocal裡面呢?因為Spring在AOP後并不能向應用程式傳遞參數,應用程式的每個業務代碼是事先定義好的,Spring并不會要求在業務代碼的入口參數中必須編寫Connection的入口參數。此時Spring選擇了ThreadLocal,通過它保證連接配接對象始終線上程内部,任何時候都能拿到,此時Spring非常清楚什麼時候回收這個連接配接,也就是非常清楚什麼時候從ThreadLocal中删除這個元素(在9.2節中會詳細講解)。

從Spring事務管理器的設計上可以看出,Spring利用ThreadLocal得到了一個很完美的設計思路,同時它在設計時也十厘清楚ThreadLocal中元素應該在什麼時候删除。由此,我們簡單地認為ThreadLocal盡量使用在一個全局的設計上,而不是一種打更新檔的間接方法。

了解了基本應用場景後,接下來看一個例子。定義一個類用于存放靜态的ThreadLocal對象,通過多個線程并行地對ThreadLocal對象進行set、get操作,并将值進行列印,來看看每個線程自己設定進去的值和取出來的值是否是一樣的。代碼如下:

代碼清單5-8 簡單的ThreadLocal例子

  1. public class ThreadLocalTest {  
  2.     static class ResourceClass {  
  3.         public final static ThreadLocal<String> RESOURCE_1 =  
  4.                                        new ThreadLocal<String>();  
  5.         public final static ThreadLocal<String> RESOURCE_2 =  
  6.     }  
  7.     static class A {  
  8.         public void setOne(String value) {  
  9.             ResourceClass.RESOURCE_1.set(value);  
  10.         }  
  11.         public void setTwo(String value) {  
  12.             ResourceClass.RESOURCE_2.set(value);  
  13.     static class B {  
  14.         public void display() {  
  15.             System.out.println(ResourceClass.RESOURCE_1.get()  
  16.                         + ":" + ResourceClass.RESOURCE_2.get());  
  17.     public static void main(String []args) {  
  18.         final A a = new A();  
  19.         final B b = new B();  
  20.         for(int i = 0 ; i < 15 ; i ++) {  
  21.             final String resouce1 = "線程-" + I;  
  22.             final String resouce2 = " value = (" + i + ")";  
  23.             new Thread() {  
  24.                 public void run() {  
  25.                 try {  
  26.                     a.setOne(resouce1);  
  27.                     a.setTwo(resouce2);  
  28.                     b.display();  
  29.                 }finally {  
  30.                     ResourceClass.RESOURCE_1.remove();  
  31.                     ResourceClass.RESOURCE_2.remove();  
  32.                 }  
  33.             }  
  34.         }.start();  
  35. }  
public class ThreadLocalTest {

	static class ResourceClass {

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

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

	}

	static class A {

		public void setOne(String value) {
			ResourceClass.RESOURCE_1.set(value);
		}

		public void setTwo(String value) {
			ResourceClass.RESOURCE_2.set(value);
		}
	}

	static class B {
		public void display() {
			System.out.println(ResourceClass.RESOURCE_1.get()
						+ ":" + ResourceClass.RESOURCE_2.get());
		}
	}

	public static void main(String []args) {
		final A a = new A();
		final B b = new B();
		for(int i = 0 ; i < 15 ; i ++) {
			final String resouce1 = "線程-" + I;
			final String resouce2 = " value = (" + i + ")";
			new Thread() {
				public void run() {
				try {
					a.setOne(resouce1);
					a.setTwo(resouce2);
					b.display();
				}finally {
					ResourceClass.RESOURCE_1.remove();
					ResourceClass.RESOURCE_2.remove();
				}
			}
		}.start();
		}
	}
}      

關于這段代碼,我們先說幾點。 ◎ 定義了兩個ThreadLocal變量,最終的目的就是要看最後兩個值是否能對應上,這樣才有機會證明ThreadLocal所儲存的資料可能是線程私有的。 ◎ 使用兩個内部類隻是為了使測試簡單,友善大家直覺了解,大家也可以将這個例子的代碼拆分到多個類中,得到的結果是相同的。 ◎ 測試代碼更像是為了友善傳遞參數,因為它确實傳遞參數很友善,但這僅僅是為了測試。 ◎ 在finally裡面有remove()操作,是為了清空資料而使用的。為何要清空資料,在後文中會繼續介紹細節。 測試結果如下: 線程-6: value = (6) 線程-9: value = (9) 線程-0: value = (0) 線程-10: value = (10) 線程-12: value = (12) 線程-14: value = (14) 線程-11: value = (11) 線程-3: value = (3) 線程-5: value = (5) 線程-13: value = (13) 線程-2: value = (2) 線程-4: value = (4) 線程-8: value = (8) 線程-7: value = (7) 線程-1: value = (1) 大家可以看到輸出的線程順序并非最初定義線程的順序,理論上可以說明多線程應當是并發執行的,但是依然可以保持每個線程裡面的值是對應的,說明這些值已經達到了線程私有的目的。 不是說共享變量無法做到線程私有嗎?它又是如何做到線程私有的呢?這就需要我們知道一點點原理上的東西,否則用起來也沒那麼放心,請看下面的介紹。

(2)ThreadLocal内在原理

從前面的操作可以發現,ThreadLocal最常見的操作就是set、get、remove三個動作,下面來看看這三個動作到底做了什麼事情。首先看set操作,源碼片段如圖5-5所示。

ThreadLocal

圖5-5 ThreadLcoal.set源碼片段 圖5-5中的第一條代碼取出了目前線程t,然後調用getMap(t)方法時傳入了目前線程,換句話說,該方法傳回的ThreadLocalMap和目前線程有點關系,我們先記錄下來。進一步判定如果這個map不為空,那麼設定到Map中的Key就是this,值就是外部傳入的參數。這個this是什麼呢?就是定義的ThreadLocal對象。 代碼中有兩條路徑需要追蹤,分别是getMap(Thread)和createMap(Thread , T)。首先來看看getMap(t)操作,如圖5-6所示。

圖5-6 getMap(Thread)操作

ThreadLocal

在這裡,我們看到ThreadLocalMap其實就是線程裡面的一個屬性,它在Thread類中的定義是: ThreadLocal.ThreadLocalMap threadLocals = null; 這種方法很容易讓人混淆,因為這個ThreadLocalMap是ThreadLocal裡面的内部類,放在了Thread類裡面作為一個屬性而存在,ThreadLocal本身成為這個Map裡面存放的Key,使用者輸入的值是Value。太亂了,理不清楚了,畫個圖來看看(見圖5-7)。 簡單來講,就是這個Map對象在Thread裡面作為私有的變量而存在,是以是線程安全的。ThreadLocal通過Thread.currentThread()擷取目前的線程就能得到這個Map對象,同時将自身作為Key發起寫入和讀取,由于将自身作為Key,是以一個ThreadLocal對象就能存放一個線程中對應的Java對象,通過get也自然能找到這個對象。

圖5-7 Thread與ThreadLocal的僞代碼關聯關系

ThreadLocal

如果還沒有了解,則可以将思維放寬一點。當定義變量String a時,這個“a”其實隻是一個名稱(在第3章中已經說到了常量池),虛拟機需要通過符号表來找到相應的資訊,而這種方式正好就像一種K-V結構,底層的處理方式也确實很接近這樣,這裡的處理方式是顯式地使用Map來存放資料,這也是一種實作手段的變通。 現在有了思路,繼續回到上面的話題,為了驗證前面的推斷和了解,來看看createMap方法的細節,如圖5-8所示。

ThreadLocal

圖5-8 createMap操作 這段代碼是執行一個建立新的Map的操作,并且将第一個值作為這個Map的初始化值,由于這個Map是線程私有的,不可能有另一個線程同時也在對它做put操作,是以這裡的指派和初始化是絕對線程安全的,也同時保證了每一個外部寫入的值都将寫入到Map對象中。 最後來看看get()、remove()代碼,或許看到這裡就可以認定我們的理論是正确的,如圖5-9所示。

ThreadLocal

圖5-9 get()/remove()方法的代碼片段 給我們的感覺是,這樣實作是一種技巧,而不是一種技術。 其實是技巧還是技術完全是從某種角度來看的,或者說是從某種抽象層次來看的,如果這段代碼在C++中實作,難道就叫技術,不是技巧了嗎?當然不是!胖哥認為技術依然是建立在思想和方法基礎上的,隻是看實作的抽象層次在什麼級别。就像在本書中多個地方探讨的一些基礎原理一樣,我們探讨了它的思想,其實它的實作也是基于某種技巧和手段的,隻是對程式封裝後就變成了某種文法和API,是以胖哥認為,一旦學會使用技巧思考問題,就學會了通過技巧去看待技術本身。我們應當通過這種設計,學會一種變通和發散的思維,學會了解各種各樣的場景,這樣便可以積累許多真正的财富,這些财富不是通過某些工具的使用或測試就可以獲得的。 ThreadLocal的這種設計很完美嗎? 不是很完美,它依然有許多坑,在這裡對它容易誤導程式員當成傳參工具就不再多提了,下面我們來看看它的使用不當會導緻什麼技術上的問題。

(3)ThreadLocal的坑

通過上面的分析,我們可以認識到ThreadLocal其實是與線程綁定的一個變量,如此就會出現一個問題:如果沒有将ThreadLocal内的變量删除(remove)或替換,它的生命周期将會與線程共存。是以,ThreadLocal的一個很大的“坑”就是當使用不當時,導緻使用者不知道它的作用域範圍。 大家可能認為線程結束後ThreadLocal應該就回收了,如果線程真的登出了确實是這樣的,但是事實有可能并非如此,例如線上程池中對線程管理都是采用線程複用的方法(Web容器通常也會采用線程池),線上程池中線程很難結束甚至于永遠不會結束,這将意味着線程持續的時間将不可預測,甚至與JVM的生命周期一緻。那麼相應的ThreadLocal變量的生命周期也将不可預測。 也許系統中定義少量幾個ThreadLocal變量也無所謂,因為每次set資料時是用ThreadLocal本身作為Key的,相同的Key肯定會替換原來的資料,原來的資料就可以被釋放了,理論上不會導緻什麼問題。但世事無絕對,如果ThreadLocal中直接或間接包裝了集合類或複雜對象,每次在同一個ThreadLocal中取出對象後,再對内容做操作,那麼内部的集合類和複雜對象所占用的空間可能會開始膨脹。 抛開代碼本身的問題,舉一個極端的例子。如果不想定義太多的ThreadLocal變量,就用一個HashMap來存放,這貌似沒什麼問題。由于ThreadLocal在程式的任何一個地方都可以用得到,在某些設計不當的代碼中很難知道這個HashMap寫入的源頭,在代碼中為了保險起見,通常會先檢查這個HashMap是否存在,若不存在,則建立一個HashMap寫進去;若存在,通常也不會替換掉,因為代碼編寫者通常會“害怕”因為這種替換會丢掉一些來自“其他地方寫入HashMap的資料”,進而導緻許多不可預見的問題。 在這樣的情況下,HashMap第一次放入ThreadLocal中也許就一直不會被釋放,而這個HashMap中可能開始存放許多Key-Value資訊,如果業務上存放的Key值在不斷變化(例如,将業務的ID作為Key),那麼這個HashMap就開始不斷變長,并且很可能在每個線程中都有一個這樣的HashMap,逐漸地形成了間接的記憶體洩漏。曾經有很多人吃過這個虧,而且吃虧的時候發現這樣的代碼可能不是在自己的業務系統中,而是出現在某些二方包、三方包中(開源并不保證沒有問題)。 要處理這種問題很複雜,不過首先要保證自己編寫的代碼是沒問題的,要保證沒問題不是說我們不去用ThreadLocal,甚至不去學習它,因為它肯定有其應用價值。在使用時要明白ThreadLocal最難以捉摸的是“不知道哪裡是源頭”(通常是代碼設計不當導緻的),隻有知道了源頭才能控制結束的部分,或者說我們從設計的角度要讓ThreadLocal的set、remove有始有終,通常在外部調用的代碼中使用finally來remove資料,隻要我們仔細思考和抽象是可以達到這個目的的。有些是二方包、三方包的問題,對于這些問題我們需要學會的是找到問題的根源後解決,關于二方包、三方包的運作跟蹤,可參看第3.7.9節介紹的BTrace工具。 補充:在任何異步程式中(包括異步I/O、非阻塞I/O),ThreadLocal的參數傳遞是不靠譜的,因為線程将請求發送後,就不再等待遠端傳回結果繼續向下執行了,真正的傳回結果得到後,處理的線程可能是另一個。

#####################################個人總結 ####################################

Thread.java源碼中:

  1. ThreadLocal.ThreadLocalMap threadLocals = null;  
ThreadLocal.ThreadLocalMap threadLocals = null;      

即:每個Thread對象都有一個ThreadLocal.ThreadLocalMap成員變量,ThreadLocal.ThreadLocalMap是一個ThreadLocal類的靜态内部類(如下所示),是以Thread類可以進行引用.

  1. static class ThreadLocalMap {  
static class ThreadLocalMap {      

是以每個線程都會有一個ThreadLocal.ThreadLocalMap對象的引用

當在ThreadLocal中進行設值的時候:

  1. public void set(T value) {  
  2.     Thread t = Thread.currentThread();  
  3.     ThreadLocalMap map = getMap(t);  
  4.     if (map != null)  
  5.         map.set(this, value);  
  6.     else  
  7.         createMap(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);
    }      
  1. ThreadLocalMap getMap(Thread t) {  
  2.     return t.threadLocals;  
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }      

首先擷取目前線程的引用,然後擷取目前線程的ThreadLocal.ThreadLocalMap對象(t.threadLocals變量就是ThreadLocal.ThreadLocalMap的變量),如果該對象為空就建立一個,如下所示:

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

這個this變量就是ThreadLocal的引用,對于同一個ThreadLocal對象每個線程都是相同的,但是每個線程各自有一個ThreadLocal.ThreadLocalMap對象儲存着各自ThreadLocal引用為key的值,是以互不影響,而且:如果你建立一個ThreadLocal的對象,這個對象還是儲存在每個線程同一個ThreadLocal.ThreadLocalMap對象之中,因為一個線程隻有一個ThreadLocal.ThreadLocalMap對象,這個對象是在第一個ThreadLocal第一次設值的時候進行建立,如上所述的createMap方法.

  1. ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {  
  2.     table = new Entry[INITIAL_CAPACITY];  
  3.     int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);  
  4.     table[i] = new Entry(firstKey, firstValue);  
  5.     size = 1;  
  6.     setThreshold(INITIAL_CAPACITY);  
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);
        }      

總結:

深入研究java.lang.ThreadLocal類:http://blog.csdn.net/xiaohulunb/article/details/19603611

API說明:

ThreadLocal(),T get(),protected T initialValue(),void remove(),void set(T value)

典型執行個體:

1.Hiberante的Session 工具類HibernateUtil

2.通過不同的線程對象設定Bean屬性,保證各個線程Bean對象的獨立性。

ThreadLocal使用的一般步驟:

[plain] view plain copy print?

  1. 1、在多線程的類(如ThreadDemo類)中,建立一個ThreadLocal對象threadXxx,用來儲存線程間需要隔離處理的對象xxx。  
  2. 2、在ThreadDemo類中,建立一個擷取要隔離通路的資料的方法getXxx(),在方法中判斷,若ThreadLocal對象為null時候,應該new()一個隔離通路類型的對象,并強制轉換為要應用的類型。  
  3. 3、在ThreadDemo類的run()方法中,通過getXxx()方法擷取要操作的資料,這樣可以保證每個線程對應一個資料對象,在任何時刻都操作的是這個對象。  
1、在多線程的類(如ThreadDemo類)中,建立一個ThreadLocal對象threadXxx,用來儲存線程間需要隔離處理的對象xxx。
2、在ThreadDemo類中,建立一個擷取要隔離通路的資料的方法getXxx(),在方法中判斷,若ThreadLocal對象為null時候,應該new()一個隔離通路類型的對象,并強制轉換為要應用的類型。
3、在ThreadDemo類的run()方法中,通過getXxx()方法擷取要操作的資料,這樣可以保證每個線程對應一個資料對象,在任何時刻都操作的是這個對象。      

與Synchonized的對比:

  1. ThreadLocal和Synchonized都用于解決多線程并發通路。但是ThreadLocal與synchronized有本質的差別。synchronized是利用鎖的機制,使變量或代碼塊在某一時該隻能被一個線程通路。而ThreadLocal為每一個線程都提供了變量的副本,使得每個線程在某一時間通路到的并不是同一個對象,這樣就隔離了多個線程對資料的資料共享。而Synchronized卻正好相反,它用于在多個線程間通信時能夠獲得資料共享。  
  2. Synchronized用于線程間的資料共享,而ThreadLocal則用于線程間的資料隔離。  
ThreadLocal和Synchonized都用于解決多線程并發通路。但是ThreadLocal與synchronized有本質的差別。synchronized是利用鎖的機制,使變量或代碼塊在某一時該隻能被一個線程通路。而ThreadLocal為每一個線程都提供了變量的副本,使得每個線程在某一時間通路到的并不是同一個對象,這樣就隔離了多個線程對資料的資料共享。而Synchronized卻正好相反,它用于在多個線程間通信時能夠獲得資料共享。
 
Synchronized用于線程間的資料共享,而ThreadLocal則用于線程間的資料隔離。      

一句話了解ThreadLocal:向ThreadLocal裡面存東西就是向它裡面的Map存東西的,然後ThreadLocal把這個Map挂到目前的線程底下,這樣Map就隻屬于這個線程了。

使用ThreadLocal改進你的層次的劃分(spring事務的實作):http://blog.csdn.net/zhouyong0/article/details/7761835

源碼剖析之ThreadLocal:http://wangxinchun.iteye.com/blog/1884228

Java中的ThreadLocal源碼解析(上):http://maosidiaoxian.iteye.com/blog/1939142