1. 程式人生 > >java 並發(七)--- ThreadLocal

java 並發(七)--- ThreadLocal

散列 integer 解決沖突 efault getent associate 原來 for pac

文章部分圖片來自參考資料

ThreadLocal 概述

ThreadLocal 線程本地變量 ,是一個工具,可以讓多個線程保持一個變量的副本,那麽每個線程可以訪問自己內部的副本變量。

ReentranReadWriteLock中。

技術分享圖片

ThreadLocal 結構圖裏面看到有兩個內部類,一個 SuppliedThreadLocal , 一個ThreadLocalMap 。下面用一張圖來說明線程使用的示意圖。可以看到每個Thread有個 ThreadLocalMap ,然後裏面由hash值分列的的數組 Entry[] 。Entry 數據結構就是圖中淡綠色框內所示。

技術分享圖片

 

ThreadLocal 源碼分析

ThreadLocal 下文簡稱 TL, TL最常見的方法就是 get 和 set 了。

  1 public void set(T value) {
  2         Thread t = Thread.currentThread();
  3         ThreadLocalMap map = getMap(t);
  4         if (map != null)
  5             map.set(this, value);
  6         else
  7             createMap(t, value);
  8
}
 
  1     public T get() {
  2         Thread t = Thread.currentThread();
  3         ThreadLocalMap map = getMap(t);
  4         if (map != null) {
  5             ThreadLocalMap.Entry e = map.getEntry(this);
  6             if (e != null) {
  7                 @SuppressWarnings("unchecked")
  8                 T result = (T)e.value;
  9
return result; 10 } 11 } 12 return setInitialValue(); 13 }
 
  1     ThreadLocalMap getMap(Thread t) {
  2         return t.threadLocals;
  3     }

  1  ThreadLocal.ThreadLocalMap threadLocals = null;

可以看到thread 內部中持有TL的內部類變量。我們來看一下 ThreadLocalMap, threadLocalMap 內部定義一個類,Entry 類。這是threadLocalMap 內的變量

  1 static class ThreadLocalMap {
  2     /**
  3      * The initial capacity -- MUST be a power of two.
  4      */
  5     private static final int INITIAL_CAPACITY = 16;
  6 
  7     /**
  8      * The table, resized as necessary.
  9      * table.length MUST always be a power of two.
 10      */
 11     private Entry[] table;
 12 
 13     /**
 14      * The number of entries in the table.
 15      */
 16     private int size = 0;
 17 
 18     /**
 19      * The next size value at which to resize.
 20      */
 21     private int threshold; // Default to 0
 22 }
 23 
  1  static class Entry extends WeakReference<ThreadLocal<?>> {
  2             /** The value associated with this ThreadLocal. */
  3             Object value;
  4 
  5             Entry(ThreadLocal<?> k, Object v) {
  6                 super(k);
  7                 value = v;
  8             }
  9 }

我們看到 TL 的set 方法實際就是調用了 ThreadLocalMap 的set 方法。

  1  private void set(ThreadLocal<?> key, Object value) {
  2 
  3             // We don‘t use a fast path as with get() because it is at
  4             // least as common to use set() to create new entries as
  5             // it is to replace existing ones, in which case, a fast
  6             // path would fail more often than not.
  7 
  8             Entry[] tab = table;
  9             int len = tab.length;
 10             int i = key.threadLocalHashCode & (len-1);
 11 
 12             for (Entry e = tab[i];
 13                  e != null;
 14                  e = tab[i = nextIndex(i, len)]) {
 15                 ThreadLocal<?> k = e.get();
 16 
 17             	//找到相同的 key 
 18                 if (k == key) {
 19                     e.value = value;
 20                     return;
 21                 }
 22 
 23                 //某個key失效
 24                 if (k == null) {
 25                     replaceStaleEntry(key, value, i);
 26                     return;
 27                 }
 28             }
 29 
 30            	//走到這裏必定是退出了循環,即是遇到空的 entry ,直接放在空的地方,檢查是否需要擴容,重新 hash 
 31             tab[i] = new Entry(key, value);
 32             int sz = ++size;
 33             if (!cleanSomeSlots(i, sz) && sz >= threshold)
 34                 rehash();
 35 }
 36 
 37 
 38   //   這個方法是替代某些失效的entry ,最終的值會放在 table[staleSlot]
 39   //  slotToExpunge 這個變量從名字上可以看出就是需要擦洗的 slot (指的是某個位置)
 40  private void replaceStaleEntry(ThreadLocal<?> key, Object value,
 41                                        int staleSlot) {
 42             Entry[] tab = table;
 43             int len = tab.length;
 44             Entry e;
 45 
 46             // Back up to check for prior stale entry in current run.
 47             // We clean out whole runs at a time to avoid continual
 48             // incremental rehashing due to garbage collector freeing
 49             // up refs in bunches (i.e., whenever the collector runs).
 50             //  向前找是否有失效節點,如果有做一下標記,即是為 slotToExpunge 賦值
 51             int slotToExpunge = staleSlot;
 52             for (int i = prevIndex(staleSlot, len);
 53                  (e = tab[i]) != null;
 54                  i = prevIndex(i, len))
 55                 if (e.get() == null)
 56                     slotToExpunge = i;
 57 
 58             // Find either the key or trailing null slot of run, whichever
 59             // occurs first
 60             //  向後尋找是否有相同的 key
 61             for (int i = nextIndex(staleSlot, len);
 62                  (e = tab[i]) != null;
 63                  i = nextIndex(i, len)) {
 64                 ThreadLocal<?> k = e.get();
 65 
 66                 // If we find key, then we need to swap it
 67                 // with the stale entry to maintain hash table order.
 68                 // The newly stale slot, or any other stale slot
 69                 // encountered above it, can then be sent to expungeStaleEntry
 70                 // to remove or rehash all of the other entries in run.
 71             	//  找到相同的值,交換位置到 tab[staleSlot]
 72                 if (k == key) {
 73                     e.value = value;
 74 
 75                     tab[i] = tab[staleSlot];
 76                     tab[staleSlot] = e;
 77 
 78                     // Start expunge at preceding stale entry if it exists
 79                     // 擦洗失效值
 80                     if (slotToExpunge == staleSlot)
 81                         slotToExpunge = i;
 82                     cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
 83                     return;
 84                 }
 85 
 86                 // If we didn‘t find stale entry on backward scan, the
 87                 // first stale entry seen while scanning for key is the
 88                 // first still present in the run.
 89                 if (k == null && slotToExpunge == staleSlot)
 90                     slotToExpunge = i;
 91             }
 92 
 93             // If key not found, put new entry in stale slot
 94             //找不到值會放在 tab[staleSlot] ,即原來失效值的位置上
 95             tab[staleSlot].value = null;
 96             tab[staleSlot] = new Entry(key, value);
 97 
 98             // If there are any other stale entries in run, expunge them
 99             // 擦洗失效值
100             if (slotToExpunge != staleSlot)
101                 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
102 }
103 

技術分享圖片

技術分享圖片

可以看到我們在 set 的時候,TL內會檢查是否存在失效值。也可以看到 ThreadLocalMap 的Hash 中解決沖突的方式只是簡單的向下尋找空的位置,即線性探測,這樣的效率比較低,所以建議 :

每個線程只存一個變量,這樣的話所有的線程存放到map中的Key都是相同的ThreadLocal,如果一個線程要保存多個變量,就需要創建多個ThreadLocal,多個ThreadLocal放入Map中時會極大的增加Hash沖突的可能。

下面看一下 get 方法,不難。

  1 // ThreadLocalMap
  2 private Entry getEntry(ThreadLocal<?> key) {
  3             int i = key.threadLocalHashCode & (table.length - 1);
  4             Entry e = table[i];
  5             if (e != null && e.get() == key)
  6                 return e;
  7             else
  8                 return getEntryAfterMiss(key, i, e);
  9 }
  1        private Entry getEntry(ThreadLocal<?> key) {
  2             int i = key.threadLocalHashCode & (table.length - 1);
  3             Entry e = table[i];
  4             if (e != null && e.get() == key)
  5                 return e;
  6             else
  7             	//獲取的時候出現失效的entry
  8                 return getEntryAfterMiss(key, i, e);
  9         }
 10 
 11 
 12         // 往後找,失效的值擦洗掉,沒有就返回 Null
 13         private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
 14             Entry[] tab = table;
 15             int len = tab.length;
 16 
 17             while (e != null) {
 18                 ThreadLocal<?> k = e.get();
 19                 if (k == key)
 20                     return e;
 21                 if (k == null)
 22                     expungeStaleEntry(i);
 23                 else
 24                     i = nextIndex(i, len);
 25                 e = tab[i];
 26             }
 27             return null;
 28         }

ThreadLocalMap 的 key 失效

ThreadLocalMap下文簡稱 TLM 。

  1 static class Entry extends WeakReference<ThreadLocal<?>> {
  2             /** The value associated with this ThreadLocal. */
  3             Object value;
  4 
  5             Entry(ThreadLocal<?> k, Object v) {
  6                 super(k);
  7                 value = v;
  8             }
  9         }

可以看到 Entry 繼承 WeakReference (弱引用)。ThreadLocal在沒有外部對象強引用時,發生GC時弱引用Key會被回收,而Value不會回收,如果創建ThreadLocal的線程一直持續運行,那麽這個Entry對象中的value就有可能一直得不到回收,於是這就存在一條強引用鏈的關系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,這條強引用鏈會導致Entry不會回收,Value也不會回收,但Entry中的Key卻已經被回收的情況,造成內存泄漏。

技術分享圖片

我們從源碼中也可以看到在 get 和 set 等方法都有檢查失效值的操作,同時當我們使用TL時,某個線程不再需要某個值的時候應該調用 remove 方法,下面代碼中 e.clear() 這一句實際是調用了弱引用的 clear 方法,實現對對象的回收。

  1         private void remove(ThreadLocal<?> key) {
  2             Entry[] tab = table;
  3             int len = tab.length;
  4             int i = key.threadLocalHashCode & (len-1);
  5             for (Entry e = tab[i];
  6                  e != null;
  7                  e = tab[i = nextIndex(i, len)]) {
  8                 if (e.get() == key) {
  9                     e.clear();
 10                     expungeStaleEntry(i);
 11                     return;
 12                 }
 13             }
 14         }
  1     /**
  2      * Clears this reference object.  Invoking this method will not cause this
  3      * object to be enqueued.
  4      *
  5      * <p> This method is invoked only by Java code; when the garbage collector
  6      * clears references it does so directly, without invoking this method.
  7      */
  8     public void clear() {
  9         this.referent = null;
 10     }

我們來看一下weakReference 表示弱引用,java中有四種引用類型,強引用,弱引用,軟引用,虛引用。

在Java語言中, 當一個對象o被創建時, 它被放在Heap裏. 當GC運行的時候, 如果發現沒有任何引用指向o, o就會被回收以騰出內存空間. 也就是說, 一個對象被回收, 必須滿足兩個條件:

  • 沒有任何引用指向它

  • GC被運行.

  1 DemoA a=new DemoA();
  2 DemoB b=new DemoB(a);

假如有下面代碼,如果我們增加一行代碼來將a對象的引用設置為null,當一個對象不再被其他對象引用的時候,是會被GC回收的,但是對於這個場景來說,即時是a=null,也不可能被回收,因為DemoB依賴DemoA,這個時候是可能造成內存泄漏的。

  1 DemoA a=new DemoA();
  2 DemoB b=new DemoB(a);
  3 a=null;

通過弱引用,有兩個方法可以避免這樣的問題。

  1 //方法1
  2 DemoA a=new DemoA();
  3 DemoB b=new DemoB(a);
  4 a=null;
  5 b=null;
  6 //方法2
  7 DemoA a=new DemoA();
  8 WeakReference b=new WeakReference(a);
  9 a=null;
 10 

對於方法2來說,DemoA只是被弱引用依賴,假設垃圾收集器在某個時間點決定一個對象是弱可達的(weakly reachable)(也就是說當前指向它的全都是弱引用),這時垃圾收集器會清除所有指向該對象的弱引用,然後把這個弱可達對象標記為可終結(finalizable)的,這樣它隨後就會被回收。

我們可以設想b就是ThreadLocal ,試想一下如果這裏沒有使用弱引用,意味著ThreadLocal的生命周期和線程是強綁定,只要線程沒有銷毀,那麽ThreadLocal一直無法回收。而使用弱引用以後,當ThreadLocal被回收時,由於Entry的key是弱引用,不會影響ThreadLocal的回收防止內存泄漏,同時,在後續的源碼分析中會看到,ThreadLocalMap本身的垃圾清理會用到這一個好處,方便對無效的Entry進行回收。

其實我們從源碼分析可以看到,ThreadLocalMap是做了防護措施的

  • 首先從ThreadLocal的直接索引位置(通過

    ThreadLocal.threadLocalHashCode & (len-1)運算得到)獲取Entry e,如果e不為null並且key相同則返回e

  • 如果e為null或者key不一致則向下一個位置查詢,如果下一個位置的key和當前需要查詢的key相等,則返回對應的Entry,否則,如果key值為null,則擦除該位置的Entry,否則繼續向下一個位置查詢

Entry 的 Hash 值

如何實現一個線程多個ThreadLocal對象,每一個ThreadLocal對象是如何區分的呢?

  1 void createMap(Thread t, T firstValue) {
  2        t.threadLocals = new ThreadLocalMap(this, firstValue);
  3 }
  1 static class ThreadLocalMap {
  2      static class Entry extends WeakReference<ThreadLocal<?>> {
  3 
  4        /** The value associated with this ThreadLocal. */
  5         Object value;
  6 
  7         Entry(ThreadLocal<?> k, Object v) {
  8                 super(k);
  9                value = v;
 10         }
 11      }
 12 
 13      ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
 14 	   //構造一個Entry數組,並設置初始大小
 15            table = new Entry[INITIAL_CAPACITY];
 16            //計算Entry數據下標
 17            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
 18 	   //將`firstValue`存入到指定的table下標中
 19            table[i] = new Entry(firstKey, firstValue);
 20            size = 1;//設置節點長度為1
 21            setThreshold(INITIAL_CAPACITY); //設置擴容的閾值
 22       }
 23 //...省略部分代碼
 24 }
 25 
 26 
  1 private final int threadLocalHashCode = nextHashCode();
  2 private static AtomicInteger nextHashCode = new AtomicInteger();
  3 private static final int HASH_INCREMENT = 0x61c88647;
  4 
  5 private static int nextHashCode() {
  6     return nextHashCode.getAndAdd(HASH_INCREMENT);
  7 }

那為什麽要使用到 0x61c88647 這個值呢? 我們首先要明白一點,散列的目的是使數據分布更加均勻。那麽這個數字的使用必定會達到這個目的。

魔數0x61c88647的選取和斐波那契散列有關,0x61c88647對應的十進制為1640531527。而斐波那契散列的乘數可以用 (long)((1L<<31)*(Math.sqrt(5)-1)); 如果把這個值給轉為帶符號的int,則會得到-1640531527。也就是說(long)((1L<<31)*(Math.sqrt(5)-1));得到的結果就是1640531527,也就是魔數0x61c88647

建議

  • 將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命周期就更長,由於一直存在ThreadLocal的強引用,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱引用訪問到Entry的value值,然後remove它,防止內存泄露

  • 每次使用完ThreadLocal,都調用它的remove()方法,清除數據。

參考資料 :

  • ThreadLocal-面試必問深度解析
  • JAVA高級架構 微信公眾號的 “ThreadLocal的使用及原理分析”
  • 從 ThreadLocal 的實現看散列算法

java 並發(七)--- ThreadLocal