1. 程式人生 > >深入分析ThreadLocal

深入分析ThreadLocal

我們在使用一個類時,首先要知道它能做什麼,然後再去深入分析它的工作原理。ThreadLocal如果從名字上來看像是“本地執行緒"的意思,其實ThreadLocal並不是一個執行緒,而是執行緒的區域性變數。當使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本。 在Java多執行緒面試題中,作者對ThreadLocal的回答是:   ThreadLocal是Java裡一種特殊的變數。每個執行緒都有一個ThreadLocal就是每個執行緒都擁有了自己獨立的一個變數,競爭條件被徹底消除了。它是為建立代價高昂的物件獲取執行緒安全的好方法,比如你可以用ThreadLocal讓SimpleDateFormat變成執行緒安全的,因為那個類建立代價高昂且每次呼叫都需要建立不同的例項所以不值得在區域性範圍使用它,如果為每個執行緒提供一個自己獨有的變數拷貝,將大大提高效率。首先,通過複用減少了代價高昂的物件的建立個數。其次,你在沒有使用高代價的同步或者不變性的情況下獲得了執行緒安全。執行緒區域性變數的另一個不錯的例子是ThreadLocalRandom類,它在多執行緒環境中減少了建立代價高昂的Random物件的個數。

官方對ThreadLocal的描述:

    該類提供了執行緒區域性 (thread-local) 變數。這些變數不同於它們的普通對應物,因為訪問某個變數(通過其 get 或 set 方法)的每個執行緒都有自己的區域性變數,它獨立於變數的初始化副本。ThreadLocal 例項通常是類中的 private static 欄位,它們希望將狀態與某一個執行緒(例如,使用者 ID 或事務 ID)相關聯。 一、ThreadLocal原始碼分析    1.1 屬性

/**  * threadLocalHashCode用來查詢Entry陣列中的元素  */ private final int threadLocalHashCode = nextHashCode();   /**  * 一個原子類,用來保證threadLocalHashCode值的安全性  * .  */ private static AtomicInteger nextHashCode = new AtomicInteger();   /**  * 表示了連續分配的兩個ThreadLocal例項的threadLocalHashCode值的增量  */ private static final int HASH_INCREMENT = 0x61c88647;    1.2 內部類ThreadLocalMap   ThreadLocalMap的實現原理跟HashMap差不多,內部有一個Entry陣列,一個Entry通常至少包括Key,Value, 查詢時通過key的Hash值和陣列長度進行計算來得到Entry在陣列中的位置,進而得到相應的value。但是比較特殊的是Entry繼承了軟引用WeakReference ,也就是Entry只能生存到下一次垃圾回收之前,並且它實際上真正弱引用的是key而不是value。 

 /**          * The entries in this hash map extend WeakReference, using          * its main ref field as the key (which is always a          * ThreadLocal object).  Note that null keys (i.e. entry.get()          * == null) mean that the key is no longer referenced, so the          * entry can be expunged from table.  Such entries are referred to          * as "stale entries" in the code that follows.          */         static class Entry extends WeakReference<ThreadLocal> {             /** The value associated with this ThreadLocal. */             Object value;               Entry(ThreadLocal k, Object v) {                 super(k);                 value = v;             }         }    1.2.1 為什麼在ThreadLocalMap中弱引用ThreadLocal物件呢?   使用弱引用的好處是能夠減少記憶體使用。在set()、put()等待操作中都有出現這段程式碼ThreadLocal k = e.get(), 通常在弱引用使用中,我們都會對其進行一個判斷,判斷其是否已經被GC回收。如果ThreadLocalMap知道Entry裡的key(ThreadLocal物件)已經被回收,那麼它對應的值也就沒有用處了。然後呼叫cleanSomeSlots()來清除相關的值,來保證ThreadLocalMap總是保持儘可能的小。 

   1.2.2 cleanSomeSlots()操作  private boolean cleanSomeSlots(int i, int n) {             boolean removed = false;             Entry[] tab = table;             int len = tab.length;             do {                 i = nextIndex(i, len);                 Entry e = tab[i];                 if (e != null && e.get() == null) {                     n = len;                     removed = true;                     i = expungeStaleEntry(i);                 }             } while ( (n >>>= 1) != 0);             return removed;         }  private int expungeStaleEntry(int staleSlot) {             Entry[] tab = table;             int len = tab.length;               // expunge entry at staleSlot             tab[staleSlot].value = null;             tab[staleSlot] = null;             size--;               // Rehash until we encounter null             Entry e;             int i;             for (i = nextIndex(staleSlot, len);                  (e = tab[i]) != null;                  i = nextIndex(i, len)) {                 ThreadLocal k = e.get();                 if (k == null) {                     e.value = null;                     tab[i] = null;                     size--;                 } else {                     int h = k.threadLocalHashCode & (len - 1);                     if (h != i) {                         tab[i] = null;                           // Unlike Knuth 6.4 Algorithm R, we must scan until                         // null because multiple entries could have been stale.                         while (tab[h] != null)                             h = nextIndex(h, len);                         tab[h] = e;                     }                 }             }             return i;         }    1.3 set()操作   hash雜湊的鍵值資料在儲存過程中可能會發生碰撞,大家知道HashMap儲存的是一個Entry鏈,當hash發生衝突後,將新的Entry存放在連結串列最前端。但是ThreadLocalMap不一樣,採用index+1作為重雜湊的hash值寫入。另外有一點需要注意key出現null的原因是由於Entry的key是繼承了軟引用,在下一次GC時不管它有沒有被引用都會被回收掉而Value沒有被回收。當出現null時,會呼叫replaceStaleEntry()方法接著迴圈尋找相同的key,如果存在,直接替換舊值。如果不存在,則在當前位置上重新建立新的Entry. //設定當前執行緒的執行緒區域性變數的值。 private void set(ThreadLocal key, Object value) {       // We don't use a fast path as with get() because it is at     // least as common to use set() to create new entries as     // it is to replace existing ones, in which case, a fast     // path would fail more often than not.       Entry[] tab = table;     int len = tab.length;     int i = key.threadLocalHashCode & (len-1);       for (Entry e = tab[i];          e != null;          e = tab[i = nextIndex(i, len)]) {         ThreadLocal k = e.get();         //替換掉舊值         if (k == key) {             e.value = value;             return;         }         //和HashMap不一樣,由於Entry key繼承了軟引用,會出現k是null的情況!所以會接著在replaceStaleEntry重新迴圈尋找相同的key         if (k == null) {             replaceStaleEntry(key, value, i);             return;         }     }       tab[i] = new Entry(key, value);     int sz = ++size;     //呼叫cleanSomeSlots()對table進行清理,如果沒有任何Entry被清理,並且表的size超過了閾值,就會呼叫rehash()方法     if (!cleanSomeSlots(i, sz) && sz >= threshold)         rehash(); }      1.4 get()操作   get()是獲得當前執行緒所對應的執行緒區域性變數。ThreadLocal和HashMap一樣也是通過hash計算Entry在table陣列中的位置,再使用key拿到對應的value。如果value為null,它會呼叫setInitialValue返回值。  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 Entry getEntry(ThreadLocal key) {       int i = key.threadLocalHashCode & (table.length - 1);       Entry e = table[i];       if (e != null && e.get() == key)             return e;       else       return getEntryAfterMiss(key, i, e);  }  private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {             Entry[] tab = table;             int len = tab.length;               while (e != null) {                 ThreadLocal k = e.get();                 if (k == key)                     return e;                 if (k == null)                     expungeStaleEntry(i);                 else                     i = nextIndex(i, len);                 e = tab[i];             }             return null;  }