ThreadLocal是什么

   ThreadLocal是thread local variable(線程局部變量)。線程局部變量(ThreadLocal)其實(shí)的功用非常簡(jiǎn)單,就是為每一個(gè)使用該變量的線程都提供一個(gè)變量值的副本,是每一個(gè)線程都可以獨(dú)立地改變自己的副本,而不會(huì)和其它線程的副本沖突。每個(gè)線程只能看到與自己相聯(lián)系的值,而不知道別的線程可能正在使用或修改它們自己的副本。從線程的角度看,就好像每一個(gè)線程都完全擁有該變量。
 ThreadLocal的設(shè)計(jì)

   首先看看ThreadLocal的接口:

public class ThreadLocal {
            
public Object get();
            
public void set(Object newValue);
            
public Object initialValue();
            }


       Object get() ; // 返回當(dāng)前線程的線程局部變量副本
           protected Object initialValue(); // 返回該線程局部變量的當(dāng)前線程的初始值
   void set(Object value); // 設(shè)置當(dāng)前線程的線程局部變量副本的值

   ThreadLocal有3個(gè)方法,其中值得注意的是initialValue(),該方法是一個(gè)protected的方法,顯然是為了子類重寫而特意實(shí)現(xiàn)的。該方法返回當(dāng)前線程在該線程局部變量的初始值,這個(gè)方法是一個(gè)延遲調(diào)用方法,在一個(gè)線程第1次調(diào)用get()或者set(Object)時(shí)才執(zhí)行,并且僅執(zhí)行1次。ThreadLocal中的確實(shí)實(shí)現(xiàn)直接返回一個(gè)null:

protected Object initialValue() { return null; }

  ThreadLocal是如何做到為每一個(gè)線程維護(hù)變量的副本的呢?其實(shí)實(shí)現(xiàn)的思路很簡(jiǎn)單,在ThreadLocal類中有一個(gè)Map,用于存儲(chǔ)每一個(gè)線程的變量的副本。比如下面的示例實(shí)現(xiàn):
public class ThreadLocal
{
  
private Map values = Collections.synchronizedMap(new HashMap());
  
public Object get()
  
{
   Thread curThread 
= Thread.currentThread(); 
   Object o 
= values.get(curThread); 
   
if (o == null && !values.containsKey(curThread))
   
{
    o 
= initialValue();
    values.put(curThread, o); 
   }

   
return o; 
  }


  
public void set(Object newValue)
  
{
   values.put(Thread.currentThread(), newValue);
  }


  
public Object initialValue()
  
{
   
return null
  }

}

這個(gè)實(shí)現(xiàn)的性能不會(huì)很好,因?yàn)槊總€(gè) get()set() 操作都需要 values 映射表上的同步,而且如果多個(gè)線程同時(shí)訪問同一個(gè) ThreadLocal ,那么將發(fā)生爭(zhēng)用。此外,這個(gè)實(shí)現(xiàn)也是不切實(shí)際的,因?yàn)橛?Thread 對(duì)象做 values 映射表中的關(guān)鍵字將導(dǎo)致無法在線程退出后對(duì) Thread 進(jìn)行垃圾回收,而且也無法對(duì)死線程的 ThreadLocal 的特定于線程的值進(jìn)行垃圾回收。




但JDK中的ThreadLocal的實(shí)現(xiàn)總體思路也類似于此。




  j2EE中的Thread

         Web容器中有三個(gè)周期request/Httpsession/application
         其中request是客戶端發(fā)出的一個(gè)請(qǐng)求,這個(gè)request的載體就是一個(gè)線程,實(shí)際等同于一個(gè)線程的生命周期。Request是封裝在線程上面一個(gè)抽象概念。而ThreadLocal則相當(dāng)于多個(gè)線程的一個(gè)共享全局變量存儲(chǔ)地,它里面保存的是和每個(gè)線程相關(guān)的狀態(tài)。所以threadLocal是為線程服務(wù)的,和線程處于一個(gè)底層位置。

         正因?yàn)镾pring/Hibernate這些框架對(duì)于狀態(tài)處理短處,所以才只能透過Web容器的request等狀態(tài)封裝,直接到底層操作與線程同一層次的threadLocal。

         當(dāng)一個(gè)線程或request結(jié)束時(shí),threadlocal中的狀態(tài)就沒有了,所以threadLocal基本類似request.setAttribute作用,threadLocal中的對(duì)象狀態(tài)的生命周期等同于request.


 ThreadLocal的使用



用 ThreadLocal 實(shí)現(xiàn)每線程 Singleton

線程局部變量常被用來描繪有狀態(tài)“單子”(Singleton) 或線程安全的共享對(duì)象,或者是通過把不安全的整個(gè)變量封裝進(jìn) ThreadLocal ,或者是通過把對(duì)象的特定于線程的狀態(tài)封裝進(jìn) ThreadLocal 。例如,在與數(shù)據(jù)庫有緊密聯(lián)系的應(yīng)用程序中,程序的很多方法可能都需要訪問數(shù)據(jù)庫。在系統(tǒng)的每個(gè)方法中都包含一個(gè) Connection 作為參數(shù)是不方便的 — 用“單子”來訪問連接可能是一個(gè)雖然更粗糙,但卻方便得多的技術(shù)。然而,多個(gè)線程不能安全地共享一個(gè) JDBC Connection 。如清單 3 所示,通過使用“單子”中的 ThreadLocal ,我們就能讓我們的程序中的任何類容易地獲取每線程 Connection 的一個(gè)引用。這樣,我們可以認(rèn)為 ThreadLocal 允許我們創(chuàng)建 每線程單子


清單 3. 把一個(gè) JDBC 連接存儲(chǔ)到一個(gè)每線程 Singleton 中

 

 public class ConnectionDispenser {
            
private static class ThreadLocalConnection extends ThreadLocal {
            
public Object initialValue() {
            
return DriverManager.getConnection(ConfigurationSingleton.getDbUrl());
            }

            }

            
private ThreadLocalConnection conn = new ThreadLocalConnection();
            
public static Connection getConnection() {
            
return (Connection) conn.get();
            }

            }

            


任何創(chuàng)建的花費(fèi)比使用的花費(fèi)相對(duì)昂貴些的有狀態(tài)或非線程安全的對(duì)象,例如 JDBC Connection 或正則表達(dá)式匹配器,都是可以使用每線程單子(singleton)技術(shù)的好地方。當(dāng)然,在類似這樣的地方,您可以使用其它技術(shù),例如用池,來安全地管理共享訪問。然而,從可伸縮性角度看,即使是用池也存在一些潛在缺陷。因?yàn)槌貙?shí)現(xiàn)必須使用同步,以維護(hù)池?cái)?shù)據(jù)結(jié)構(gòu)的完整性,如果所有線程使用同一個(gè)池,那么在有很多線程頻繁地對(duì)池進(jìn)行訪問的系統(tǒng)中,程序性能將因爭(zhēng)用而降低。
 


用 ThreadLocal 簡(jiǎn)化調(diào)試日志紀(jì)錄

其它適合使用 ThreadLocal 但用池卻不能成為很好的替代技術(shù)的應(yīng)用程序包括存儲(chǔ)或累積每線程上下文信息以備稍后檢索之用這樣的應(yīng)用程序。例如,假設(shè)您想創(chuàng)建一個(gè)用于管理多線程應(yīng)用程序調(diào)試信息的工具。您可以用如清單 4 所示的 DebugLogger 類作為線程局部容器來累積調(diào)試信息。在一個(gè)工作單元的開頭,您清空容器,而當(dāng)一個(gè)錯(cuò)誤出現(xiàn)時(shí),您查詢?cè)撊萜饕詸z索這個(gè)工作單元迄今為止生成的所有調(diào)試信息。


清單 4. 用 ThreadLocal 管理每線程調(diào)試日志

 

   public class DebugLogger {
            
private static class ThreadLocalList extends ThreadLocal {
            
public Object initialValue() {
            
return new ArrayList();
            }

            
public List getList() {
            
return (List) super.get();
            }

            }

            
private ThreadLocalList list = new ThreadLocalList();
            
private static String[] stringArray = new String[0];
            
public void clear() {
            list.getList().clear();
            }

            
public void put(String text) {
            list.getList().add(text);
            }

            
public String[] get() {
            
return list.getList().toArray(stringArray);
            }

            }

            


在您的代碼中,您可以調(diào)用 DebugLogger.put() 來保存您的程序正在做什么的信息,而且,稍后如果有必要(例如發(fā)生了一個(gè)錯(cuò)誤),您能夠容易地檢索與某個(gè)特定線程相關(guān)的調(diào)試信息。 與簡(jiǎn)單地把所有信息轉(zhuǎn)儲(chǔ)到一個(gè)日志文件,然后努力找出哪個(gè)日志記錄來自哪個(gè)線程(還要擔(dān)心線程爭(zhēng)用日志紀(jì)錄對(duì)象)相比,這種技術(shù)簡(jiǎn)便得多,也有效得多。

ThreadLocal 在基于 servlet 的應(yīng)用程序或工作單元是一個(gè)整體請(qǐng)求的任何多線程應(yīng)用程序服務(wù)器中也是很有用的,因?yàn)樵谔幚碚?qǐng)求的整個(gè)過程中將要用到單個(gè)線程。您可以通過前面講述的每線程單子技術(shù)用 ThreadLocal 變量來存儲(chǔ)各種每請(qǐng)求(per-request)上下文信息。






   如果希望線程局部變量初始化其它值,那么需要自己實(shí)現(xiàn)ThreadLocal的子類并重寫該方法,通常使用一個(gè)內(nèi)部匿名類對(duì)ThreadLocal進(jìn)行子類化,比如下面的例子,SerialNum類為每一個(gè)類分配一個(gè)序號(hào):

public class SerialNum
{
  // The next serial number to be assigned

  private static int nextSerialNum = 0;
  private static ThreadLocal serialNum = new ThreadLocal()
  {
   protected synchronized Object initialValue()
   {
    return new Integer(nextSerialNum++);
   }
  };

  public static int get()
  {
   return ((Integer) (serialNum.get())).intValue();
  }
}

  SerialNum類的使用將非常地簡(jiǎn)單,因?yàn)間et()方法是static的,所以在需要獲取當(dāng)前線程的序號(hào)時(shí),簡(jiǎn)單地調(diào)用:

int serial = SerialNum.get();

  即可。

   在線程是活動(dòng)的并且ThreadLocal對(duì)象是可訪問的時(shí),該線程就持有一個(gè)到該線程局部變量副本的隱含引用,當(dāng)該線程運(yùn)行結(jié)束后,該線程擁有的所以線程局部變量的副本都將失效,并等待垃圾收集器收集。

   ThreadLocal與其它同步機(jī)制的比較

   ThreadLocal和其它同步機(jī)制相比有什么優(yōu)勢(shì)呢?ThreadLocal和其它所有的同步機(jī)制都是為了解決多線程中的對(duì)同一變量的訪問沖突,在普通的同步機(jī)制中,是通過對(duì)象加鎖來實(shí)現(xiàn)多個(gè)線程對(duì)同一變量的安全訪問的。這時(shí)該變量是多個(gè)線程共享的,使用這種同步機(jī)制需要很細(xì)致地分析在什么時(shí)候?qū)ψ兞窟M(jìn)行讀寫,什么時(shí)候需要鎖定某個(gè)對(duì)象,什么時(shí)候釋放該對(duì)象的鎖等等很多。所有這些都是因?yàn)槎鄠€(gè)線程共享了資源造成的。ThreadLocal就從另一個(gè)角度來解決多線程的并發(fā)訪問,ThreadLocal會(huì)為每一個(gè)線程維護(hù)一個(gè)和該線程綁定的變量的副本,從而隔離了多個(gè)線程的數(shù)據(jù),每一個(gè)線程都擁有自己的變量副本,從而也就沒有必要對(duì)該變量進(jìn)行同步了。ThreadLocal提供了線程安全的共享對(duì)象,在編寫多線程代碼時(shí),可以把不安全的整個(gè)變量封裝進(jìn)ThreadLocal,或者把該對(duì)象的特定于線程的狀態(tài)封裝進(jìn)ThreadLocal。

   由于ThreadLocal中可以持有任何類型的對(duì)象,所以使用ThreadLocal get當(dāng)前線程的值是需要進(jìn)行強(qiáng)制類型轉(zhuǎn)換。但隨著新的Java版本(1.5)將模版的引入,新的支持模版參數(shù)的ThreadLocal<T>類將從中受益。也可以減少強(qiáng)制類型轉(zhuǎn)換,并將一些錯(cuò)誤檢查提前到了編譯期,將一定程度地簡(jiǎn)化ThreadLocal的使用。

   總結(jié)

   當(dāng)然ThreadLocal并不能替代同步機(jī)制,兩者面向的問題領(lǐng)域不同。同步機(jī)制是為了同步多個(gè)線程對(duì)相同資源的并發(fā)訪問,是為了多個(gè)線程之間進(jìn)行通信的有效方式;而ThreadLocal是隔離多個(gè)線程的數(shù)據(jù)共享,從根本上就不在多個(gè)線程之間共享資源(變量),這樣當(dāng)然不需要對(duì)多個(gè)線程進(jìn)行同步了。所以,如果你需要進(jìn)行多個(gè)線程之間進(jìn)行通信,則使用同步機(jī)制;如果需要隔離多個(gè)線程之間的共享沖突,可以使用ThreadLocal,這將極大地簡(jiǎn)化你的程序,使程序更加易讀、簡(jiǎn)潔。


實(shí)例:
public class Hello implements Runnable{
    
static class Counter{
        
int c = 0;
        
public void Count(){
            System.
out.println(Thread.currentThread().hashCode() + ""+ c++);
        }

    }

    
private static ThreadLocal counter = new ThreadLocal(){
        
protected Object initialValue() {return new Counter();}
    }
;
    
public static Counter Counter(){return (Counter)counter.get();} 
    
    
public static void main(String[] args)throws Exception {
        Hello h 
= new Hello();
        h.run();
        
new Thread(new Hello()).start();
        Thread.sleep(
1000L);
        h.run();
        Counter().Count();
    }


    
public void run() {
        Counter().Count();
        Counter().Count();
        Counter().Count();
    }

}


結(jié)果:
253585550
253585551
253585552
263995540
263995541
263995542
253585553
253585554
253585555
253585556

看樣ThreadLocal工作的很好.請(qǐng)注意ThreadLocal類似一個(gè)訪問媒介,get()返回的值是存儲(chǔ)在當(dāng)前線程中的.

值得注意的是InheritableThreadLocal類,可以使ThreadLocal數(shù)據(jù)傳遞到子線程中.

另外servlet線程可能會(huì)被重新利用,要注意在每個(gè)servlet開始時(shí)重新初始化.