前面我們介紹了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(),對應的含義分別是:
返回此線程局部變量的當前線程副本中的值、將此線程局部變量的當前線程副本中的值設置為指定值、移除此線程局部變量當前線程的值、返回此線程局部變量的當前線程的“初始值”。
轉載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
還記得我們在第三篇的上半節引出的那個例子么?幾個線程修改同一個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());

    }
}
轉載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/

貌似這個程序沒什么問題。但是運行結果卻顯示:這個程序中的3個線程會拋出3個空指針異常。讀者一定感到很困惑。我明明在構造器當中把Student對象set進了ThreadLocal里面阿,為什么run起來之后居然在調用stuLocal.get()方法的時候得到的是NULL呢?
轉載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
帶著這個疑問,讓我們深入到JDK的代碼當中,去一看究竟。
轉載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
原來,在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。
轉載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
講到這里,我想不少朋友一定已經看出來了:ThreadLocal的initialValue()方法是需要被覆蓋的。
轉載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
于是,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,必須注意以下幾點:
轉載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
1. 總是對ThreadLocal中的initialValue()方法進行覆蓋。
轉載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
2. 當使用set()或get()方法時牢記這兩個方法是對當前活動線程中的ThreadLocalMap進行操作,一定要認清哪個是當前活動線程!
轉載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
3. 適當的使用泛型,可以減少不必要的類型轉換以及可能由此產生的問題。
轉載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
運行該程序,我們發現:程序的執行過程只需要5秒,而如果采用同步的方法,程序的執行結果相同,但執行時間需要15秒。以前是多個線程為了爭取一個資源,不得不在同步規則的制約下互相謙讓,浪費了一些時間。
轉載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
現在,采用ThreadLocal機制以后,可用的資源多了,你有我有全都有,所以,每個線程都可以毫無顧忌的工作,自然就提高了并發性,線程安全也得以保證。

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

把ThreadLocal這樣的話題放在我們的同步機制探討中似乎顯得不是很合適。但是ThreadLocal的確為我們解決多線程的并發問題帶來了全新的思路。它為每個線程創建一個獨立的資源副本,從而將多個線程中的數據隔離開來,避免了同步所產生的性能問題,是一種“以空間換時間”的解決方案。
但這并不是說ThreadLocal就是包治百病的萬能藥了。如果實際的情況不允許我們為每個線程分配一個本地資源副本的話,同步還是非常有意義的。
轉載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
好了,本系列到此馬上就要劃上一個圓滿的句號了。不知大家有什么意見和疑問沒有。希望看到你們的留言。

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