深入研究java.lang.ThreadLocal類

idaretobe發表於2015-01-14
一、概述
 
ThreadLocal是什麼呢?其實ThreadLocal並非是一個執行緒的本地實現版本,它並不是一個Thread,而是threadlocalvariable(執行緒區域性變數)。也許把它命名為ThreadLocalVar更加合適。執行緒區域性變數(ThreadLocal)其實的功用非常簡單,就是為每一個使用該變數的執行緒都提供一個變數值的副本,是Java中一種較為特殊的執行緒繫結機制,是每一個執行緒都可以獨立地改變自己的副本,而不會和其它執行緒的副本衝突。
 
從執行緒的角度看,每個執行緒都保持一個對其執行緒區域性變數副本的隱式引用,只要執行緒是活動的並且 ThreadLocal 例項是可訪問的;線上程消失之後,其執行緒區域性例項的所有副本都會被垃圾回收(除非存在對這些副本的其他引用)。
 
通過ThreadLocal存取的資料,總是與當前執行緒相關,也就是說,JVM 為每個執行的執行緒,繫結了私有的本地例項存取空間,從而為多執行緒環境常出現的併發訪問問題提供了一種隔離機制。
 
ThreadLocal是如何做到為每一個執行緒維護變數的副本的呢?其實實現的思路很簡單,在ThreadLocal類中有一個Map,用於儲存每一個執行緒的變數的副本。
 
概括起來說,對於多執行緒資源共享的問題,同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。前者僅提供一份變數,讓不同的執行緒排隊訪問,而後者為每一個執行緒都提供了一份變數,因此可以同時訪問而互不影響。
 
二、API說明
 
ThreadLocal()
          建立一個執行緒本地變數。
 
T get()
          返回此執行緒區域性變數的當前執行緒副本中的值,如果這是執行緒第一次呼叫該方法,則建立並初始化此副本。
 
protected  T initialValue()
          返回此執行緒區域性變數的當前執行緒的初始值。最多在每次訪問執行緒來獲得每個執行緒區域性變數時呼叫此方法一次,即執行緒第一次使用 get() 方法訪問變數的時候。如果執行緒先於 get 方法呼叫 set(T) 方法,則不會線上程中再呼叫 initialValue 方法。
 
   若該實現只返回 null;如果程式設計師希望將執行緒區域性變數初始化為 null 以外的某個值,則必須為 ThreadLocal 建立子類,並重寫此方法。通常,將使用匿名內部類。initialValue 的典型實現將呼叫一個適當的構造方法,並返回新構造的物件。
 
void remove()
          移除此執行緒區域性變數的值。這可能有助於減少執行緒區域性變數的儲存需求。如果再次訪問此執行緒區域性變數,那麼在預設情況下它將擁有其 initialValue。
 
void set(T value)
          將此執行緒區域性變數的當前執行緒副本中的值設定為指定值。許多應用程式不需要這項功能,它們只依賴於 initialValue() 方法來設定執行緒區域性變數的值。
 
在程式中一般都重寫initialValue方法,以給定一個特定的初始值。
 
 
三、典型例項
 
1、Hiberante的Session 工具類HibernateUtil
這個類是Hibernate官方文件中HibernateUtil類,用於session管理。
 
public class HibernateUtil {
    private static Log log = LogFactory.getLog(HibernateUtil.class);
    private static final SessionFactory sessionFactory;     //定義SessionFactory
 
    static {
        try {
            // 通過預設配置檔案hibernate.cfg.xml建立SessionFactory
            sessionFactory = new Configuration().configure().buildSessionFactory();
        } catch (Throwable ex) {
            log.error("初始化SessionFactory失敗!", ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    //建立執行緒區域性變數session,用來儲存Hibernate的Session
    public static final ThreadLocal session = new ThreadLocal();
 
    /**
     * 獲取當前執行緒中的Session
     * @return Session
     * @throws HibernateException
     */
    public static Session currentSession() throws HibernateException {
        Session s = (Session) session.get();
        // 如果Session還沒有開啟,則新開一個Session
        if (s == null) {
            s = sessionFactory.openSession();
            session.set(s);         //將新開的Session儲存到執行緒區域性變數中
        }
        return s;
    }
 
    public static void closeSession() throws HibernateException {
        //獲取執行緒區域性變數,並強制轉換為Session型別
        Session s = (Session) session.get();
        session.set(null);
        if (s != null)
            s.close();
    }
}
 
在這個類中,由於沒有重寫ThreadLocal的initialValue()方法,則首次建立執行緒區域性變數session其初始值為null,第一次呼叫currentSession()的時候,執行緒區域性變數的get()方法也為null。因此,對session做了判斷,如果為null,則新開一個Session,並儲存到執行緒區域性變數session中,這一步非常的關鍵,這也是“public static final ThreadLocal session = new ThreadLocal()”所建立物件session能強制轉換為Hibernate Session物件的原因。
 
2、另外一個例項
建立一個Bean,通過不同的執行緒物件設定Bean屬性,保證各個執行緒Bean物件的獨立性。
 
/**
 * Created by IntelliJ IDEA.
 * User: leizhimin
 * Date: 2007-11-23
 * Time: 10:45:02
 * 學生
 */
public class Student {
    private int age = 0;   //年齡
 
    public int getAge() {
        return this.age;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
}
 
/**
 * Created by IntelliJ IDEA.
 * User: leizhimin
 * Date: 2007-11-23
 * Time: 10:53:33
 * 多執行緒下測試程式
 */
public class ThreadLocalDemo implements Runnable {
    //建立執行緒區域性變數studentLocal,在後面你會發現用來儲存Student物件
    private final static ThreadLocal studentLocal = new ThreadLocal();
 
    public static void main(String[] agrs) {
        ThreadLocalDemo td = new ThreadLocalDemo();
        Thread t1 = new Thread(td, "a");
        Thread t2 = new Thread(td, "b");
        t1.start();
        t2.start();
    }
 
    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 student = getStudent();
        student.setAge(age);
        System.out.println("thread " + currentThreadName + " first read age is:" + student.getAge());
        try {
            Thread.sleep(500);
        }
        catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge());
    }
 
    protected Student getStudent() {
        //獲取本地執行緒變數並強制轉換為Student型別
        Student student = (Student) studentLocal.get();
        //執行緒首次執行此方法的時候,studentLocal.get()肯定為null
        if (student == null) {
            //建立一個Student物件,並儲存到本地執行緒變數studentLocal中
            student = new Student();
            studentLocal.set(student);
        }
        return student;
    }
}
 
執行結果:
a is running! 
thread a set age to:76 
b is running! 
thread b set age to:27 
thread a first read age is:76 
thread b first read age is:27 
thread a second read age is:76 
thread b second read age is:27 
 
可以看到a、b兩個執行緒age在不同時刻列印的值是完全相同的。這個程式通過妙用ThreadLocal,既實現多執行緒併發,遊兼顧資料的安全性。
 
四、總結
 
ThreadLocal使用場合主要解決多執行緒中資料資料因併發產生不一致問題。ThreadLocal為每個執行緒的中併發訪問的資料提供一個副本,通過訪問副本來執行業務,這樣的結果是耗費了記憶體,單大大減少了執行緒同步所帶來效能消耗,也減少了執行緒併發控制的複雜度。
 
ThreadLocal不能使用原子型別,只能使用Object型別。ThreadLocal的使用比synchronized要簡單得多。
 
ThreadLocal和Synchonized都用於解決多執行緒併發訪問。但是ThreadLocal與synchronized有本質的區別。synchronized是利用鎖的機制,使變數或程式碼塊在某一時該只能被一個執行緒訪問。而ThreadLocal為每一個執行緒都提供了變數的副本,使得每個執行緒在某一時間訪問到的並不是同一個物件,這樣就隔離了多個執行緒對資料的資料共享。而Synchronized卻正好相反,它用於在多個執行緒間通訊時能夠獲得資料共享。
 
Synchronized用於執行緒間的資料共享,而ThreadLocal則用於執行緒間的資料隔離。
 
當然ThreadLocal並不能替代synchronized,它們處理不同的問題域。Synchronized用於實現同步機制,比ThreadLocal更加複雜。
 
 
五、ThreadLocal使用的一般步驟
 
1、在多執行緒的類(如ThreadDemo類)中,建立一個ThreadLocal物件threadXxx,用來儲存執行緒間需要隔離處理的物件xxx。
2、在ThreadDemo類中,建立一個獲取要隔離訪問的資料的方法getXxx(),在方法中判斷,若ThreadLocal物件為null時候,應該new()一個隔離訪問型別的物件,並強制轉換為要應用的型別。

3、在ThreadDemo類的run()方法中,通過getXxx()方法獲取要操作的資料,這樣可以保證每個執行緒對應一個資料物件,在任何時刻都操作的是這個物件。


首先,ThreadLocal 不是用來解決共享物件的多執行緒訪問問題的,一般情況下,通過ThreadLocal.set() 到執行緒中的物件是該執行緒自己使用的物件,其他執行緒是不需要訪問的,也訪問不到的。各個執行緒中訪問的是不同的物件。 

另外,說ThreadLocal使得各執行緒能夠保持各自獨立的一個物件,並不是通過ThreadLocal.set()來實現的,而是通過每個執行緒中的new 物件 的操作來建立的物件,每個執行緒建立一個,不是什麼物件的拷貝或副本。通過ThreadLocal.set()將這個新建立的物件的引用儲存到各執行緒的自己的一個map中,每個執行緒都有這樣一個map,執行ThreadLocal.get()時,各執行緒從自己的map中取出放進去的物件,因此取出來的是各自自己執行緒中的物件,ThreadLocal例項是作為map的key來使用的。 

如果ThreadLocal.set()進去的東西本來就是多個執行緒共享的同一個物件,那麼多個執行緒的ThreadLocal.get()取得的還是這個共享物件本身,還是有併發訪問問題。 

下面來看一個hibernate中典型的ThreadLocal的應用: 

Java程式碼  收藏程式碼
  1. private static final ThreadLocal threadSession = new ThreadLocal();  
  2.   
  3. public static Session getSession() throws InfrastructureException {  
  4.     Session s = (Session) threadSession.get();  
  5.     try {  
  6.         if (s == null) {  
  7.             s = getSessionFactory().openSession();  
  8.             threadSession.set(s);  
  9.         }  
  10.     } catch (HibernateException ex) {  
  11.         throw new InfrastructureException(ex);  
  12.     }  
  13.     return s;  
  14. }  

可以看到在getSession()方法中,首先判斷當前執行緒中有沒有放進去session,如果還沒有,那麼通過sessionFactory().openSession()來建立一個session,再將session set到執行緒中,實際是放到當前執行緒的ThreadLocalMap這個map中,這時,對於這個session的唯一引用就是當前執行緒中的那個ThreadLocalMap(下面會講到),而threadSession作為這個值的key,要取得這個session可以通過threadSession.get()來得到,裡面執行的操作實際是先取得當前執行緒中的ThreadLocalMap,然後將threadSession作為key將對應的值取出。這個session相當於執行緒的私有變數,而不是public的。 
顯然,其他執行緒中是取不到這個session的,他們也只能取到自己的ThreadLocalMap中的東西。要是session是多個執行緒共享使用的,那還不亂套了。 
試想如果不用ThreadLocal怎麼來實現呢?可能就要在action中建立session,然後把session一個個傳到service和dao中,這可夠麻煩的。或者可以自己定義一個靜態的map,將當前thread作為key,建立的session作為值,put到map中,應該也行,這也是一般人的想法,但事實上,ThreadLocal的實現剛好相反,它是在每個執行緒中有一個map,而將ThreadLocal例項作為key,這樣每個map中的項數很少,而且當執行緒銷燬時相應的東西也一起銷燬了,不知道除了這些還有什麼其他的好處。 

總之,ThreadLocal不是用來解決物件共享訪問問題的,而主要是提供了保持物件的方法和避免引數傳遞的方便的物件訪問方式。歸納了兩點: 
1。每個執行緒中都有一個自己的ThreadLocalMap類物件,可以將執行緒自己的物件保持到其中,各管各的,執行緒可以正確的訪問到自己的物件。 
2。將一個共用的ThreadLocal靜態例項作為key,將不同物件的引用儲存到不同執行緒的ThreadLocalMap中,然後線上程執行的各處通過這個靜態ThreadLocal例項的get()方法取得自己執行緒儲存的那個物件,避免了將這個物件作為引數傳遞的麻煩。
 

當然如果要把本來執行緒共享的物件通過ThreadLocal.set()放到執行緒中也可以,可以實現避免引數傳遞的訪問方式,但是要注意get()到的是那同一個共享物件,併發訪問問題要靠其他手段來解決。但一般來說執行緒共享的物件通過設定為某類的靜態變數就可以實現方便的訪問了,似乎沒必要放到執行緒中。 

ThreadLocal的應用場合,我覺得最適合的是按執行緒多例項(每個執行緒對應一個例項)的物件的訪問,並且這個物件很多地方都要用到。 

下面來看看ThreadLocal的實現原理(jdk1.5原始碼) 
Java程式碼  收藏程式碼
  1. public class ThreadLocal<T> {  
  2.     /** 
  3.      * ThreadLocals rely on per-thread hash maps attached to each thread 
  4.      * (Thread.threadLocals and inheritableThreadLocals).  The ThreadLocal 
  5.      * objects act as keys, searched via threadLocalHashCode.  This is a 
  6.      * custom hash code (useful only within ThreadLocalMaps) that eliminates 
  7.      * collisions in the common case where consecutively constructed 
  8.      * ThreadLocals are used by the same threads, while remaining well-behaved 
  9.      * in less common cases. 
  10.      */  
  11.     private final int threadLocalHashCode = nextHashCode();  
  12.   
  13.     /** 
  14.      * The next hash code to be given out. Accessed only by like-named method. 
  15.      */  
  16.     private static int nextHashCode = 0;  
  17.   
  18.     /** 
  19.      * The difference between successively generated hash codes - turns 
  20.      * implicit sequential thread-local IDs into near-optimally spread 
  21.      * multiplicative hash values for power-of-two-sized tables. 
  22.      */  
  23.     private static final int HASH_INCREMENT = 0x61c88647;  
  24.   
  25.     /** 
  26.      * Compute the next hash code. The static synchronization used here 
  27.      * should not be a performance bottleneck. When ThreadLocals are 
  28.      * generated in different threads at a fast enough rate to regularly 
  29.      * contend on this lock, memory contention is by far a more serious 
  30.      * problem than lock contention. 
  31.      */  
  32.     private static synchronized int nextHashCode() {  
  33.         int h = nextHashCode;  
  34.         nextHashCode = h + HASH_INCREMENT;  
  35.         return h;  
  36.     }  
  37.   
  38.     /** 
  39.      * Creates a thread local variable. 
  40.      */  
  41.     public ThreadLocal() {  
  42.     }  
  43.   
  44.     /** 
  45.      * Returns the value in the current thread's copy of this thread-local 
  46.      * variable.  Creates and initializes the copy if this is the first time 
  47.      * the thread has called this method. 
  48.      * 
  49.      * @return the current thread's value of this thread-local 
  50.      */  
  51.     public T get() {  
  52.         Thread t = Thread.currentThread();  
  53.         ThreadLocalMap map = getMap(t);  
  54.         if (map != null)  
  55.             return (T)map.get(this);  
  56.   
  57.         // Maps are constructed lazily.  if the map for this thread  
  58.         // doesn't exist, create it, with this ThreadLocal and its  
  59.         // initial value as its only entry.  
  60.         T value = initialValue();  
  61.         createMap(t, value);  
  62.         return value;  
  63.     }  
  64.   
  65.     /** 
  66.      * Sets the current thread's copy of this thread-local variable 
  67.      * to the specified value.  Many applications will have no need for 
  68.      * this functionality, relying solely on the {@link #initialValue} 
  69.      * method to set the values of thread-locals. 
  70.      * 
  71.      * @param value the value to be stored in the current threads' copy of 
  72.      *        this thread-local. 
  73.      */  
  74.     public void set(T value) {  
  75.         Thread t = Thread.currentThread();  
  76.         ThreadLocalMap map = getMap(t);  
  77.         if (map != null)  
  78.             map.set(this, value);  
  79.         else  
  80.             createMap(t, value);  
  81.     }  
  82.   
  83.     /** 
  84.      * Get the map associated with a ThreadLocal. Overridden in 
  85.      * InheritableThreadLocal. 
  86.      * 
  87.      * @param  t the current thread 
  88.      * @return the map 
  89.      */  
  90.     ThreadLocalMap getMap(Thread t) {  
  91.         return t.threadLocals;  
  92.     }  
  93.   
  94.     /** 
  95.      * Create the map associated with a ThreadLocal. Overridden in 
  96.      * InheritableThreadLocal. 
  97.      * 
  98.      * @param t the current thread 
  99.      * @param firstValue value for the initial entry of the map 
  100.      * @param map the map to store. 
  101.      */  
  102.     void createMap(Thread t, T firstValue) {  
  103.         t.threadLocals = new ThreadLocalMap(this, firstValue);  
  104.     }  
  105.   
  106.     .......  
  107.   
  108.     /** 
  109.      * ThreadLocalMap is a customized hash map suitable only for 
  110.      * maintaining thread local values. No operations are exported 
  111.      * outside of the ThreadLocal class. The class is package private to 
  112.      * allow declaration of fields in class Thread.  To help deal with 
  113.      * very large and long-lived usages, the hash table entries use 
  114.      * WeakReferences for keys. However, since reference queues are not 
  115.      * used, stale entries are guaranteed to be removed only when 
  116.      * the table starts running out of space. 
  117.      */  
  118.     static class ThreadLocalMap {  
  119.   
  120.     ........  
  121.   
  122.     }  
  123.   
  124. }  


可以看到ThreadLocal類中的變數只有這3個int型: 
Java程式碼  收藏程式碼
  1. private final int threadLocalHashCode = nextHashCode();  
  2. private static int nextHashCode = 0;  
  3. private static final int HASH_INCREMENT = 0x61c88647;  

而作為ThreadLocal例項的變數只有 threadLocalHashCode 這一個,nextHashCode 和HASH_INCREMENT 是ThreadLocal類的靜態變數,實際上HASH_INCREMENT是一個常量,表示了連續分配的兩個ThreadLocal例項的threadLocalHashCode值的增量,而nextHashCode 的表示了即將分配的下一個ThreadLocal例項的threadLocalHashCode 的值。 

可以來看一下建立一個ThreadLocal例項即new ThreadLocal()時做了哪些操作,從上面看到建構函式ThreadLocal()裡什麼操作都沒有,唯一的操作是這句: 
Java程式碼  收藏程式碼
  1. private final int threadLocalHashCode = nextHashCode();  

那麼nextHashCode()做了什麼呢: 
Java程式碼  收藏程式碼
  1. private static synchronized int nextHashCode() {  
  2.     int h = nextHashCode;  
  3.     nextHashCode = h + HASH_INCREMENT;  
  4.     return h;  
  5. }  
就是將ThreadLocal類的下一個hashCode值即nextHashCode的值賦給例項的threadLocalHashCode,然後nextHashCode的值增加HASH_INCREMENT這個值。 

因此ThreadLocal例項的變數只有這個threadLocalHashCode,而且是final的,用來區分不同的ThreadLocal例項,ThreadLocal類主要是作為工具類來使用,那麼ThreadLocal.set()進去的物件是放在哪兒的呢? 

看一下上面的set()方法,兩句合併一下成為 
Java程式碼  收藏程式碼
  1. ThreadLocalMap map = Thread.currentThread().threadLocals;  

這個ThreadLocalMap 類是ThreadLocal中定義的內部類,但是它的例項卻用在Thread類中: 
Java程式碼  收藏程式碼
  1. public class Thread implements Runnable {  
  2.     ......  
  3.   
  4.     /* ThreadLocal values pertaining to this thread. This map is maintained 
  5.      * by the ThreadLocal class. */  
  6.     ThreadLocal.ThreadLocalMap threadLocals = null;    
  7.     ......  
  8. }  


再看這句: 
Java程式碼  收藏程式碼
  1. if (map != null)  
  2.     map.set(this, value);  

也就是將該ThreadLocal例項作為key,要保持的物件作為值,設定到當前執行緒的ThreadLocalMap 中,get()方法同樣大家看了程式碼也就明白了,ThreadLocalMap 類的程式碼太多了,我就不帖了,自己去看原始碼吧。 


相關文章