初識ThreadLocal

最近公司在進行Java開發人員的招聘活動,其中有一道面試題是這樣的:“請簡單描述一下ThreadLocal類的作用。” 結果發現有很多的面試者沒有聽說過ThreadLocal或者聽說過卻不知道這個類究竟是用來做什麼的。 因此這裏寫一篇博客來介紹一下ThreadLocal這個類。

 

在我們日常的項目開發中,ThreadLocal並不是一個經常使用的類。它更多的是被用在諸如Spring,Tomcat或者是Hibernate這些封裝了多線程併發的框架或是容器中。而它的目的也正是爲了解決多線程併發訪問共享數據的問題。
 
儘管普通開發人員很少有機會涉及到它,瞭解ThreadLocal也依然有助於他們來學習Java併發編程。通過閱讀ThreadLocal的源碼並瞭解它解決併發問題的思路,開發人員可以更好的理解代碼中遇到的多線程bug,更不用提那些在項目開發中需要用到多線程編程的開發人員了。因此,不論你是否用到了ThreadLocal類,都很有必要學習一下它。
 
在我們討論代碼細節之前,先來看看java concurrent in practice中對於多線程併發問題的描述:
     “所有的多線程問題都可以歸結爲多個線程訪問共享可變狀態時的管理問題。”
 
這裏的狀態也就我們說的數據。這句話說明多線程問題必須在以下三個條件都滿足的時候纔會發生:
1. 擁有多個線程
2. 共享狀態
3. 該狀態可變
 
如果其中任何一個條件沒有辦法滿足,都不會出現多線程問題:
1. 只有單一的線程。 很顯然,這並沒有多線程問題。

2. 共享狀態不可變。 假設某條數據被多線程共享,然而該數據是不可變數據,那麼它便沒有多線程問題。舉例來說: Java中的String類型就是不可變的,因此String的共享並不會導致多線程安全問題。

3. 多線程不共享狀態。 任意的數據都由某個線程獨佔,不與其他線程分享,因此也不會出現多線程問題。
 
那麼相應的,解決多線程問題的辦法有以下幾種:
1. 在訪問狀態變量時使用同步。這是最基本的想法,任何一本Java多線程編程的書都會詳細描述如何在Java中使用同步,這裏不再贅述。
2. 將狀態變量修改爲不可變的變量。許多新的編程語言,諸如Scala,便是採用這樣的辦法來解決多線程問題的。
3. 避免線程之間共享狀態變量。 我們今天討論的ThreadLocal,便是屬於此類解決辦法。
 
剛剛接觸ThreadLocal的同學經常會問這樣一個問題:“ThreadLocal是線程安全的麼?” 這個問題很難回答,因爲當你問這個問題的時候,便默認的認爲ThreadLocal是爲了解決多線程之間共享狀態的訪問問題的。雖然ThreadLocal的目的正是如此,但是它所採用的辦法是“避免多線程之間共享狀態”。既然沒有了多線程的共享狀態,也就無所謂是否線程安全了。因此不能簡單的說ThreadLocal是否線程安全,這個問題其實沒有意義。
那麼ThreadLocal是如何做到“避免多線程之間的狀態共享”的呢?通過在內部維護一個(當前線程 ->對象)的映射表,每個線程都只能訪問到映射到自己線程的對象,而無法訪問其它線程的對象。通過這種方法,避免了多線程之間的狀態共享,自然也就無所謂線程安全問題了。
 
如果你將某個對象的引用擴散到多個線程中,並將其設置到ThreadLocal裏,那麼多個線程所指向的便是同一個對象,對它的訪問當然也是有線程安全問題的。從這個角度來講,ThreadLocal並不是線程安全的。
 
換一個角度來描述: TheadLocal並沒有真正解決多線程共享狀態的安全問題,它只是通過避免狀態共享的辦法規避了多線程安全問題。
 
我們來看一下ThreadLocal的源碼(JDK1.6):
Java代碼  收藏代碼
  1. /** 
  2.  * Returns the value in the current thread's copy of this 
  3.  * thread-local variable.  If the variable has no value for the 
  4.  * current thread, it is first initialized to the value returned 
  5.  * by an invocation of the {@link #initialValue} method. 
  6.  * 
  7.  * @return the current thread's value of this thread-local 
  8.  */  
  9. public T get() {  
  10.     Thread t = Thread.currentThread();  
  11.     ThreadLocalMap map = getMap(t);  
  12.     if (map != null) {  
  13.         ThreadLocalMap.Entry e = map.getEntry(this);  
  14.         if (e != null)  
  15.             return (T)e.value;  
  16.     }  
  17.     return setInitialValue();  
  18. }  
 
可以看到,當ThreadLocal的get方法被調用時,首先利用當前線程作爲key獲得了一個map,而這個map便是當前線程專屬的,其它線程無法訪問。在從該Map中找到相應的對象並返回。
 
而set方法正好相反:
Java代碼  收藏代碼
  1. /** 
  2.  * Sets the current thread's copy of this thread-local variable 
  3.  * to the specified value.  Most subclasses will have no need to  
  4.  * override this method, relying solely on the {@link #initialValue} 
  5.  * method to set the values of thread-locals. 
  6.  * 
  7.  * @param value the value to be stored in the current thread's copy of 
  8.  *        this thread-local. 
  9.  */  
  10. public void set(T value) {  
  11.     Thread t = Thread.currentThread();  
  12.     ThreadLocalMap map = getMap(t);  
  13.     if (map != null)  
  14.         map.set(this, value);  
  15.     else  
  16.         createMap(t, value);  
  17. }  
 

我們來看一個在Hibernate中使用ThreadLocal的例子:

Java代碼  收藏代碼
  1. private static ThreadLocal<Connection> connectionHolder  
  2.        = new ThreadLoca<Connection>() {  
  3.   
  4.           public Connection initialValue() {  
  5.                return DriverManager.getConnection(DB_URL);  
  6.           }  
  7.      };  
  8. public static Connection getConnection() {  
  9.      return ConnectionHolder.get();  
  10. }  

 

 

上面的例子是一個最經典的ThreadLocal使用案例: 在單線程中創建一個單例變量,並在程序啓動時初始化該單例變量,從而避免在調用每個方法時都要傳遞該變量。然而該單例可能並不是線程安全的,因此,當多線程應用程序在沒有互相協作的情況下,可以通過將該單例變量保存到ThreadLocal中,以確保每一個線程都擁有屬於自己的實例。

 

在這裏,一個更好理解的說法便是:該單例是一個線程內單例,在多線程應用中,每個線程裏都有一個該單例。

 

通過學習ThreadLocal,我們能夠對正確的在項目中使用它,同時,也能夠幫助我們對多線程編程有一個更深的認識.


(該文同時發表在http://mabusyao.iteye.com/blog/2224898)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章