程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java多線程同步問題的探究(五)

Java多線程同步問題的探究(五)

編輯:關於JAVA

五、你有我有全都有—— ThreadLocal如何解決並發安全性?

前面我們介紹了Java當中多個線程搶占一個共享資源的問題。但不論是同步還是重入鎖,都不能實實在在的解決資源緊缺的情況,這些 方案只是靠制定規則來約束線程的行為,讓它們不再拼命的爭搶,而不是真正從實質上解決他們對資源的需求。

在JDK 1.2當中,引入了java.lang.ThreadLocal。它為我們提供了一種全新的思路來解決線程並發的問題。但是他的名字難免讓我們望 文生義:本地線程?

什麼是本地線程?

本地線程開玩笑的說:不要迷戀哥,哥只是個傳說。

其實ThreadLocal並非Thread at Local,而是LocalVariable in a Thread。

根據WikiPedia上的介紹,ThreadLocal其實是源於一項多線程技術,叫做Thread Local Storage,即線程本地存儲技術。不僅僅是Java ,在C++、C#、.NET、Python、Ruby、Perl等開發平台上,該技術都已經得以實現。

當使用ThreadLocal維護變量時,它會為每個使用該變量的線程提供獨立的變量副本。也就是說,他從根本上解決的是資源數量的問題 ,從而使得每個線程持有相對獨立的資源。這樣,當多個線程進行工作的時候,它們不需要糾結於同步的問題,於是性能便大大提升。但 資源的擴張帶來的是更多的空間消耗,ThreadLocal就是這樣一種利用空間來換取時間的解決方案。

說了這麼多,來看看如何正確使用ThreadLocal。

通過研究JDK文檔,我們知道,ThreadLocal中有幾個重要的方法:get()、set()、remove()、initailValue(),對應的含義分別是:

返回此線程局部變量的當前線程副本中的值、將此線程局部變量的當前線程副本中的值設置為指定值、移除此線程局部變量當前線程的 值、返回此線程局部變量的當前線程的“初始值”。

還記得我們在第三篇的上半節引出的那個例子麼?幾個線程修改同一個Student對象中的age屬性。為了保證這幾個線程能夠工作正常, 我們需要對Student的對象進行同步。

下面我們對這個程序進行一點小小的改造,我們通過繼承Thread來實現多線程:

/**
  *
  * @author x-spirit
  */
public class ThreadDemo3 extends Thread{
     private ThreadLocal<Student> stuLocal = new ThreadLocal<Student>();
     public ThreadDemo3(Student stu){
         stuLocal.set(stu);
     }
     public static void main(String[] args) {
         Student stu = new Student();
         ThreadDemo3 td31 = new ThreadDemo3(stu);
         ThreadDemo3 td32 = new ThreadDemo3(stu);
         ThreadDemo3 td33 = new ThreadDemo3(stu);
         td31.start();
         td32.start();
         td33.start();
     }
     @Override
     public void run() {
         accessStudent();
     }
     public void accessStudent() {
         String currentThreadName = Thread.currentThread().getName();
         System.out.println(currentThreadName + " is running!");
         Random random = new Random();
         int age = random.nextInt(100);
         System.out.println("thread " + currentThreadName + " set age to:" + age);
         Student student = stuLocal.get();
         student.setAge(age);
         System.out.println("thread " + currentThreadName + " first  read age is:" +  student.getAge());
         try {
             Thread.sleep(5000);
         } catch (InterruptedException ex) {
             ex.printStackTrace();
         }
         System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge ());
     }
}

貌似這個程序沒什麼問題。但是運行結果卻顯示:這個程序中的3個線程會拋出3個空指針異常。讀者一定感到很困惑。我明明在構造器 當中把Student對象 set進了ThreadLocal裡面阿,為什麼run起來之後居然在調用stuLocal.get()方法的時候得到的是NULL呢?

帶著這個疑問,讓我們深入到JDK的代碼當中,去一看究竟。

原來,在ThreadLocal中,有一個內部類叫做ThreadLocalMap。這個ThreadLocalMap並非java.util.Map的一個實現,而是利用 java.lang.ref.WeakReference實現的一個鍵-值對應的數據結構其中,key是ThreadLocal類型,而value是Object類型,我們可以簡單的視 為HashMap<ThreadLocal,Object>。

而在每一個Thread對象中,都有一個ThreadLocalMap的引用,即Thread.threadLocals。而ThreadLocal的 set方法就是首先嘗試從當前 線程中取得ThreadLocalMap(以下簡稱Map)對象。如果取到的不為null,則以ThreadLocal對象自身為key,來取Map中的value。如果取不 到Map對象,則首先為當前線程創建一個ThreadLocalMap,然後以ThreadLocal 對象自身為key,將傳入的value放入該Map中。

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

而get方法則是首先得到當前線程的ThreadLocalMap對象,然後,根據ThreadLocal對象自身,取出相應的value。當然,如果在當前線 程中取不到ThreadLocalMap對象,則嘗試為當前線程創建ThreadLocalMap對象,並以ThreadLocal對象自身為 key,把initialValue()方法 產生的對象作為value放入新創建的ThreadLocalMap中。

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();
     }
     private T setInitialValue() {
         T value = initialValue();
         Thread t = Thread.currentThread();
         ThreadLocalMap map = getMap(t);
         if (map != null)
             map.set(this, value);
         else
             createMap(t, value);
         return value;
     }

     protected T initialValue() {
         return null;
     }

這樣,我們就明白上面的問題出在哪裡:我們在main方法執行期間,試圖在調用ThreadDemo3的構造器時向ThreadLocal置入 Student對 象,而此時,以ThreadLocal對象為key,Student對象為value的Map是被放入當前的活動線程內的。也就是 Main線程。而當我們的3個 ThreadDemo3線程運行起來以後,調用get()方法,都是試圖從當前的活動線程中取得 ThreadLocalMap對象,但當前的活動線程顯然已經不 是Main線程了,於是,程序最終執行了ThreadLocal原生的 initialValue()方法,返回了null。

講到這裡,我想不少朋友一定已經看出來了:ThreadLocal的initialValue()方法是需要被覆蓋的。

於是,ThreadLocal的正確使用方法是:將ThreadLocal以內部類的形式進行繼承,並覆蓋原來的initialValue()方法,在這裡產生可供 線程擁有的本地變量值。

這樣,我們就有了下面的正確例程:

/**
  *
  * @author x-spirit
  */
public class ThreadDemo3 extends Thread{
     private ThreadLocal<Student> stuLocal = new ThreadLocal<Student>(){
         @Override
         protected Student initialValue() {
             return new Student();
         }
     };
     public ThreadDemo3(){

     }
     public static void main(String[] args) {
         ThreadDemo3 td31 = new ThreadDemo3();
         ThreadDemo3 td32 = new ThreadDemo3();
         ThreadDemo3 td33 = new ThreadDemo3();
         td31.start();
         td32.start();
         td33.start();
     }
     @Override
     public void run() {
         accessStudent();
     }
     public void accessStudent() {
         String currentThreadName = Thread.currentThread().getName();
         System.out.println(currentThreadName + " is running!");
         Random random = new Random();
         int age = random.nextInt(100);
         System.out.println("thread " + currentThreadName + " set age to:" + age);
         Student student = stuLocal.get();
         student.setAge(age);
         System.out.println("thread " + currentThreadName + " first  read age is:" +  student.getAge());
         try {
             Thread.sleep(5000);
         } catch (InterruptedException ex) {
             ex.printStackTrace();
         }
         System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge ());
     }
}

********** 補疑 ******************

有的童鞋可能會問:“你這個Demo根本沒體現出來,每個線程裡都有一個ThreadLocal對象;應該是一個ThreadLocal對象對應多個線程 ,你這變成了一對一,完全沒體現出ThreadLocal的作用。”

那麼我們來看一下如何用一個ThreadLocal對象來對應多個線程:

/** *//**
  *
  * @author x-spirit
  */
public class ThreadDemo3 implements Runnable{
     private ThreadLocal<Student> stuLocal = new ThreadLocal<Student>(){
         @Override
         protected Student initialValue() {
             return new Student();
         }
     };
     public ThreadDemo3(){

     }
     public static void main(String[] args) {
         ThreadDemo3 td3 = new ThreadDemo3();
         Thread t1 = new Thread(td3);
         Thread t2 = new Thread(td3);
         Thread t3 = new Thread(td3);
         t1.start();
         t2.start();
         t3.start();
     }
     @Override
     public void run() {
         accessStudent();
     }
     public void accessStudent() {
         String currentThreadName = Thread.currentThread().getName();
         System.out.println(currentThreadName + " is running!");
         Random random = new Random();
         int age = random.nextInt(100);
         System.out.println("thread " + currentThreadName + " set age to:" + age);
         Student student = stuLocal.get();
         student.setAge(age);
         System.out.println("thread " + currentThreadName + " first  read age is:" +  student.getAge());
         try {
             Thread.sleep(5000);
         } catch (InterruptedException ex) {
             ex.printStackTrace();
         }
         System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge ());
     }
}

這裡,多個線程對象都使用同一個實現了Runnable接口的ThreadDemo3對象來構造。這樣,多個線程使用的ThreadLocal對象就是同一個 。結果仍然是正確的。但是仔細回想一下,這兩種實現方案有什麼不同呢?

答案其實很簡單,並沒有本質上的不同。對於第一種實現,不同的線程對象當中ThreadLocalMap裡面的KEY使用的是不同的 ThreadLocal對象。而對於第二種實現,不同的線程對象當中ThreadLocalMap裡面的KEY是同一個ThreadLocal對象。但是從本質上講,不同 的線程對象都是利用其自身的ThreadLocalMap對象來對各自的Student對象進行封裝,用ThreadLocal對象作為該ThreadLocalMap的KEY。所 以說,“ThreadLocal的思想精髓就是為每個線程創建獨立的資源副本。”這句話並不應當被理解成:一定要使用同一個ThreadLocal對象 來對多個線程進行處理。因為真正用來封裝變量的不是ThreadLocal。就算是你的程序中所有線程都共用同一個ThreadLocal對象,而你真 正封裝到ThreadLocalMap中去的仍然是.hashCode()方法返回不同值的不同對象。就好比線程就是房東,ThreadLocalMap就是房東的房子。 房東通過ThreadLocal這個中介去和房子裡的房客打交道,而房東不管要讓房客住進去還是搬出來,都首先要經過ThreadLocal這個中介。

所以提到ThreadLocal,我們不應當顧名思義的認為JDK裡面提供ThreadLocal就是提供了一個用來封裝本地線程存儲的容器,它本身並 沒有Map那樣的容器功能。真正發揮作用的是ThreadLocalMap。也就是說,事實上,采用ThreadLocal來提高並發行,首先要理解,這不是 一種簡單的對象封裝,而是一套機制,而這套機制中的三個關鍵因素(Thread、ThreadLocal、ThreadLocalMap)之間的關系是值得我們引 起注意的。

**************** 補疑完畢 ***************************

可見,要正確使用ThreadLocal,必須注意以下幾點:

1. 總是對ThreadLocal中的initialValue()方法進行覆蓋。

2. 當使用set()或get()方法時牢記這兩個方法是對當前活動線程中的ThreadLocalMap進行操作,一定要認清哪個是當前活動線程!

3. 適當的使用泛型,可以減少不必要的類型轉換以及可能由此產生的問題。

運行該程序,我們發現:程序的執行過程只需要5秒,而如果采用同步的方法,程序的執行結果相同,但執行時間需要15秒。以前是多 個線程為了爭取一個資源,不得不在同步規則的制約下互相謙讓,浪費了一些時間。

現在,采用ThreadLocal機制以後,可用的資源多了,你有我有全都有,所以,每個線程都可以毫無顧忌的工作,自然就提高了並發性 ,線程安全也得以保證。

當今很多流行的開源框架也采用ThreadLocal機制來解決線程的並發問題。比如大名鼎鼎的 Struts 2.x 和 Spring 等。

把ThreadLocal這樣的話題放在我們的同步機制探討中似乎顯得不是很合適。但是ThreadLocal的確為我們解決多線程的並發問題帶來了 全新的思路。它為每個線程創建一個獨立的資源副本,從而將多個線程中的數據隔離開來,避免了同步所產生的性能問題,是一種“以空 間換時間”的解決方案。

但這並不是說ThreadLocal就是包治百病的萬能藥了。如果實際的情況不允許我們為每個線程分配一個本地資源副本的話,同步還是非 常有意義的。

好了,本系列到此馬上就要劃上一個圓滿的句號了。不知大家有什麼意見和疑問沒有。希望看到你們的留言。

下一講中我們就來對之前的內容進行一個總結,順便討論一下被遺忘的volatile關鍵字。敬請期待。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved