前面我們介紹了Java當(dāng)中多個(gè)線程搶占一個(gè)共享資源的問(wèn)題。但不論是同步還是重入鎖,都不能實(shí)實(shí)在在的解決資源緊缺的情況,這些方案只是靠制定規(guī)則來(lái)約束線程的行為,讓它們不再拼命的爭(zhēng)搶,而不是真正從實(shí)質(zhì)上解決他們對(duì)資源的需求。

在JDK 1.2當(dāng)中,引入了java.lang.ThreadLocal。它為我們提供了一種全新的思路來(lái)解決線程并發(fā)的問(wèn)題。但是他的名字難免讓我們望文生義:本地線程?

什么是本地線程?
本地線程開(kāi)玩笑的說(shuō):不要迷戀哥,哥只是個(gè)傳說(shuō)。

其實(shí)ThreadLocal并非Thread at Local,而是LocalVariable in a Thread。

根據(jù)WikiPedia上的介紹,ThreadLocal其實(shí)是源于一項(xiàng)多線程技術(shù),叫做Thread Local Storage,即線程本地存儲(chǔ)技術(shù)。不僅僅是Java,在C++、C#、.NET、Python、Ruby、Perl等開(kāi)發(fā)平臺(tái)上,該技術(shù)都已經(jīng)得以實(shí)現(xiàn)。

當(dāng)使用ThreadLocal維護(hù)變量時(shí),它會(huì)為每個(gè)使用該變量的線程提供獨(dú)立的變量副本。也就是說(shuō),他從根本上解決的是資源數(shù)量的問(wèn)題,從而使得每個(gè)線程持有相對(duì)獨(dú)立的資源。這樣,當(dāng)多個(gè)線程進(jìn)行工作的時(shí)候,它們不需要糾結(jié)于同步的問(wèn)題,于是性能便大大提升。但資源的擴(kuò)張帶來(lái)的是更多的空間消耗,ThreadLocal就是這樣一種利用空間來(lái)?yè)Q取時(shí)間的解決方案。

說(shuō)了這么多,來(lái)看看如何正確使用ThreadLocal。

通過(guò)研究JDK文檔,我們知道,ThreadLocal中有幾個(gè)重要的方法:get()、set()、remove()、initailValue(),對(duì)應(yīng)的含義分別是:
返回此線程局部變量的當(dāng)前線程副本中的值、將此線程局部變量的當(dāng)前線程副本中的值設(shè)置為指定值、移除此線程局部變量當(dāng)前線程的值、返回此線程局部變量的當(dāng)前線程的“初始值”。
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
還記得我們?cè)诘谌纳习牍?jié)引出的那個(gè)例子么?幾個(gè)線程修改同一個(gè)Student對(duì)象中的age屬性。為了保證這幾個(gè)線程能夠工作正常,我們需要對(duì)Student的對(duì)象進(jìn)行同步。
下面我們對(duì)這個(gè)程序進(jìn)行一點(diǎn)小小的改造,我們通過(guò)繼承Thread來(lái)實(shí)現(xiàn)多線程:
/**
 *
 * 
@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());

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

貌似這個(gè)程序沒(méi)什么問(wèn)題。但是運(yùn)行結(jié)果卻顯示:這個(gè)程序中的3個(gè)線程會(huì)拋出3個(gè)空指針異常。讀者一定感到很困惑。我明明在構(gòu)造器當(dāng)中把Student對(duì)象set進(jìn)了ThreadLocal里面阿,為什么run起來(lái)之后居然在調(diào)用stuLocal.get()方法的時(shí)候得到的是NULL呢?
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
帶著這個(gè)疑問(wèn),讓我們深入到JDK的代碼當(dāng)中,去一看究竟。
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
原來(lái),在ThreadLocal中,有一個(gè)內(nèi)部類叫做ThreadLocalMap。這個(gè)ThreadLocalMap并非java.util.Map的一個(gè)實(shí)現(xiàn),而是利用java.lang.ref.WeakReference實(shí)現(xiàn)的一個(gè)鍵-值對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)其中,key是ThreadLocal類型,而value是Object類型,我們可以簡(jiǎn)單的視為HashMap<ThreadLocal,Object>。

而在每一個(gè)Thread對(duì)象中,都有一個(gè)ThreadLocalMap的引用,即Thread.threadLocals。而ThreadLocal的set方法就是首先嘗試從當(dāng)前線程中取得ThreadLocalMap(以下簡(jiǎn)稱Map)對(duì)象。如果取到的不為null,則以ThreadLocal對(duì)象自身為key,來(lái)取Map中的value。如果取不到Map對(duì)象,則首先為當(dāng)前線程創(chuàng)建一個(gè)ThreadLocalMap,然后以ThreadLocal對(duì)象自身為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方法則是首先得到當(dāng)前線程的ThreadLocalMap對(duì)象,然后,根據(jù)ThreadLocal對(duì)象自身,取出相應(yīng)的value。當(dāng)然,如果在當(dāng)前線程中取不到ThreadLocalMap對(duì)象,則嘗試為當(dāng)前線程創(chuàng)建ThreadLocalMap對(duì)象,并以ThreadLocal對(duì)象自身為key,把initialValue()方法產(chǎn)生的對(duì)象作為value放入新創(chuàng)建的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;
    }


這樣,我們就明白上面的問(wèn)題出在哪里:我們?cè)趍ain方法執(zhí)行期間,試圖在調(diào)用ThreadDemo3的構(gòu)造器時(shí)向ThreadLocal置入Student對(duì)象,而此時(shí),以ThreadLocal對(duì)象為key,Student對(duì)象為value的Map是被放入當(dāng)前的活動(dòng)線程內(nèi)的。也就是Main線程。而當(dāng)我們的3個(gè)ThreadDemo3線程運(yùn)行起來(lái)以后,調(diào)用get()方法,都是試圖從當(dāng)前的活動(dòng)線程中取得ThreadLocalMap對(duì)象,但當(dāng)前的活動(dòng)線程顯然已經(jīng)不是Main線程了,于是,程序最終執(zhí)行了ThreadLocal原生的initialValue()方法,返回了null。
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
講到這里,我想不少朋友一定已經(jīng)看出來(lái)了:ThreadLocal的initialValue()方法是需要被覆蓋的。
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
于是,ThreadLocal的正確使用方法是:將ThreadLocal以內(nèi)部類的形式進(jìn)行繼承,并覆蓋原來(lái)的initialValue()方法,在這里產(chǎn)生可供線程擁有的本地變量值。
這樣,我們就有了下面的正確例程:

/**
 *
 * 
@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());

    }
}

********** 補(bǔ)疑 ******************

有的童鞋可能會(huì)問(wèn):“你這個(gè)Demo根本沒(méi)體現(xiàn)出來(lái),每個(gè)線程里都有一個(gè)ThreadLocal對(duì)象;應(yīng)該是一個(gè)ThreadLocal對(duì)象對(duì)應(yīng)多個(gè)線程,你這變成了一對(duì)一,完全沒(méi)體現(xiàn)出ThreadLocal的作用。”

那么我們來(lái)看一下如何用一個(gè)ThreadLocal對(duì)象來(lái)對(duì)應(yīng)多個(gè)線程:

/**
 *
 * 
@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());

    }

}

這里,多個(gè)線程對(duì)象都使用同一個(gè)實(shí)現(xiàn)了Runnable接口的ThreadDemo3對(duì)象來(lái)構(gòu)造。這樣,多個(gè)線程使用的ThreadLocal對(duì)象就是同一個(gè)。結(jié)果仍然是正確的。但是仔細(xì)回想一下,這兩種實(shí)現(xiàn)方案有什么不同呢?

答案其實(shí)很簡(jiǎn)單,并沒(méi)有本質(zhì)上的不同。對(duì)于第一種實(shí)現(xiàn),不同的線程對(duì)象當(dāng)中ThreadLocalMap里面的KEY使用的是不同的ThreadLocal對(duì)象。而對(duì)于第二種實(shí)現(xiàn),不同的線程對(duì)象當(dāng)中ThreadLocalMap里面的KEY是同一個(gè)ThreadLocal對(duì)象。但是從本質(zhì)上講,不同的線程對(duì)象都是利用其自身的ThreadLocalMap對(duì)象來(lái)對(duì)各自的Student對(duì)象進(jìn)行封裝,用ThreadLocal對(duì)象作為該ThreadLocalMap的KEY。所以說(shuō),“ThreadLocal的思想精髓就是為每個(gè)線程創(chuàng)建獨(dú)立的資源副本。”這句話并不應(yīng)當(dāng)被理解成:一定要使用同一個(gè)ThreadLocal對(duì)象來(lái)對(duì)多個(gè)線程進(jìn)行處理。因?yàn)檎嬲脕?lái)封裝變量的不是ThreadLocal。就算是你的程序中所有線程都共用同一個(gè)ThreadLocal對(duì)象,而你真正封裝到ThreadLocalMap中去的仍然是.hashCode()方法返回不同值的不同對(duì)象。就好比線程就是房東,ThreadLocalMap就是房東的房子。房東通過(guò)ThreadLocal這個(gè)中介去和房子里的房客打交道,而房東不管要讓房客住進(jìn)去還是搬出來(lái),都首先要經(jīng)過(guò)ThreadLocal這個(gè)中介。

所以提到ThreadLocal,我們不應(yīng)當(dāng)顧名思義的認(rèn)為JDK里面提供ThreadLocal就是提供了一個(gè)用來(lái)封裝本地線程存儲(chǔ)的容器,它本身并沒(méi)有Map那樣的容器功能。真正發(fā)揮作用的是ThreadLocalMap。也就是說(shuō),事實(shí)上,采用ThreadLocal來(lái)提高并發(fā)行,首先要理解,這不是一種簡(jiǎn)單的對(duì)象封裝,而是一套機(jī)制,而這套機(jī)制中的三個(gè)關(guān)鍵因素(Thread、ThreadLocal、ThreadLocalMap)之間的關(guān)系是值得我們引起注意的。

**************** 補(bǔ)疑完畢 ***************************

可見(jiàn),要正確使用ThreadLocal,必須注意以下幾點(diǎn):
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
1. 總是對(duì)ThreadLocal中的initialValue()方法進(jìn)行覆蓋。
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
2. 當(dāng)使用set()或get()方法時(shí)牢記這兩個(gè)方法是對(duì)當(dāng)前活動(dòng)線程中的ThreadLocalMap進(jìn)行操作,一定要認(rèn)清哪個(gè)是當(dāng)前活動(dòng)線程!
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
3. 適當(dāng)?shù)氖褂梅盒停梢詼p少不必要的類型轉(zhuǎn)換以及可能由此產(chǎn)生的問(wèn)題。
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
運(yùn)行該程序,我們發(fā)現(xiàn):程序的執(zhí)行過(guò)程只需要5秒,而如果采用同步的方法,程序的執(zhí)行結(jié)果相同,但執(zhí)行時(shí)間需要15秒。以前是多個(gè)線程為了爭(zhēng)取一個(gè)資源,不得不在同步規(guī)則的制約下互相謙讓,浪費(fèi)了一些時(shí)間。
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
現(xiàn)在,采用ThreadLocal機(jī)制以后,可用的資源多了,你有我有全都有,所以,每個(gè)線程都可以毫無(wú)顧忌的工作,自然就提高了并發(fā)性,線程安全也得以保證。

當(dāng)今很多流行的開(kāi)源框架也采用ThreadLocal機(jī)制來(lái)解決線程的并發(fā)問(wèn)題。比如大名鼎鼎的 Struts 2.x 和 Spring 等。

把ThreadLocal這樣的話題放在我們的同步機(jī)制探討中似乎顯得不是很合適。但是ThreadLocal的確為我們解決多線程的并發(fā)問(wèn)題帶來(lái)了全新的思路。它為每個(gè)線程創(chuàng)建一個(gè)獨(dú)立的資源副本,從而將多個(gè)線程中的數(shù)據(jù)隔離開(kāi)來(lái),避免了同步所產(chǎn)生的性能問(wèn)題,是一種“以空間換時(shí)間”的解決方案。
但這并不是說(shuō)ThreadLocal就是包治百病的萬(wàn)能藥了。如果實(shí)際的情況不允許我們?yōu)槊總€(gè)線程分配一個(gè)本地資源副本的話,同步還是非常有意義的。
轉(zhuǎn)載注明出處:http://x- spirit.javaeye.com/、http: //www.tkk7.com/zhangwei217245/
好了,本系列到此馬上就要?jiǎng)澤弦粋€(gè)圓滿的句號(hào)了。不知大家有什么意見(jiàn)和疑問(wèn)沒(méi)有。希望看到你們的留言。

下一講中我們就來(lái)對(duì)之前的內(nèi)容進(jìn)行一個(gè)總結(jié),順便討論一下被遺忘的volatile關(guān)鍵字。敬請(qǐng)期待。