1. 程式人生 > >ThreadLocal終極原始碼剖析-一篇足矣!

ThreadLocal終極原始碼剖析-一篇足矣!

正文

本文較深入的分析了ThreadLocal和InheritableThreadLocal,從4個方向去分析:原始碼註釋、原始碼剖析、功能測試、應用場景。

回到頂部

一、ThreadLocal

我們使用ThreadLocal解決執行緒區域性變數統一定義問題,多執行緒資料不能共享。(InheritableThreadLocal特例除外)不能解決併發問題。解決了:基於類級別的變數定義,每一個執行緒單獨維護自己執行緒內的變數值(存、取、刪的功能

根據原始碼,畫出原理圖如下:

注意點:

1.ThreadLocal類封裝了getMap()、Set()、Get()、Remove()4個核心方法。

2.通過getMap

()獲取每個子執行緒Thread持有自己的ThreadLocalMap例項, 因此它們是不存在併發競爭的。可以理解為每個執行緒有自己的變數副本。

3.ThreadLocalMap中Entry[]陣列儲存資料,初始化長度16,後續每次都是2倍擴容。主執行緒中定義了幾個變數,Entry[]才有幾個key。

4.Entry的key是對ThreadLocal的弱引用,當拋棄掉ThreadLocal物件時,垃圾收集器會忽略這個key的引用而清理掉ThreadLocal物件, 防止了記憶體洩漏。

1.1原始碼註釋

理解原理最好的方法是看原始碼註釋:

複製程式碼
1 This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private
static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID). 2 3 For example, the class below generates unique identifiers local to each thread. A thread's id is assigned the first time it invokes ThreadId.get() and remains unchanged on subsequent calls.
複製程式碼

這個類提供執行緒區域性變數。這些變數與正常的變數不同,每個執行緒訪問一個(通過它的get或set方法)都有它自己的、獨立初始化的變數副本。ThreadLocal例項通常是類中的私有靜態欄位,希望將狀態與執行緒關聯(例如,使用者ID或事務ID)。

註釋中的示例程式碼:

下圖ThreadId類會在每個執行緒中生成唯一識別符號。執行緒的id在第一次呼叫threadid.get()時被分配,在隨後的呼叫中保持不變。

 ThreadId類利用AtomicInteger原子方法getAndIncrement,為每個執行緒建立一個threadId變數,例如第一個執行緒是1,第二個執行緒是2...,並提供一個類靜態get方法用以獲取當前執行緒ID。:

複製程式碼
 1 import java.util.concurrent.atomic.AtomicInteger;
 2 
 3  public class ThreadId {
 4      // Atomic integer containing the next thread ID to be assigned
 5      private static final AtomicInteger nextId = new AtomicInteger(0);
 6 
 7      // Thread local variable containing each thread's ID
 8      private static final ThreadLocal<Integer> threadId =
 9          new ThreadLocal<Integer>() {
10              @Override protected Integer initialValue() {
11                  return nextId.getAndIncrement();
12          }
13      };
14 
15      // Returns the current thread's unique ID, assigning it if necessary
16      public static int get() {
17          return threadId.get();
18      }
19  }
複製程式碼

如上圖,有一個注意點是:使用者可以自定義initialValue()初始化方法,來初始化threadLocal的值。

1.2 原始碼剖析

我們來追蹤一下ThreadLocal原始碼:

複製程式碼
 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     }
14 
21     private T setInitialValue() {
22         T value = initialValue();
23         Thread t = Thread.currentThread();
24         ThreadLocalMap map = getMap(t);
25         if (map != null)
26             map.set(this, value);
27         else
28             createMap(t, value);
29         return value;
30     }
31 
41     public void set(T value) {
42         Thread t = Thread.currentThread();
43         ThreadLocalMap map = getMap(t);
44         if (map != null)
45             map.set(this, value);
46         else
47             createMap(t, value);
48     }
49 
61      public void remove() {
62          ThreadLocalMap m = getMap(Thread.currentThread());
63          if (m != null)
64              m.remove(this);
65      }
66 
74     ThreadLocalMap getMap(Thread t) {
75         return t.threadLocals;
76     }
複製程式碼

看原始碼我們知道不管是set、get、remove操作的都是ThreadLocalMap,key=當前執行緒,value=執行緒區域性變數快取值。

上圖getMap最終呼叫的Thread的成員變數 ThreadLocal.ThreadLocalMap threadLocals,如下圖:

ThreadLocalMap是ThreadLocal的一個內部類,原始碼註釋:

ThreadLocalMap是一個定製的雜湊對映,僅適用於維護執行緒本地值。ThreadLocalMap類是包私有的,允許在Thread類中宣告欄位。為了幫助處理非常大且長時間的使用,雜湊表entry使用了對鍵的弱引用。有助於GC回收。

雜湊演算法-魔數0x61c88647

 ThreadLocal中定義了一個AtomicInteger,一個魔數0x61c88647,利用一定演算法實現了元素的完美雜湊。

原始碼中元素雜湊演算法如下:

1.求hashCode = i*HASH_INCREMENT+HASH_INCREMENT每次新增一個元素(threadLocal)進Entry[],自增0x61c88647
2.元素雜湊位置(陣列下標)= hashCode & (length-1),

下面校驗演算法的雜湊性:

複製程式碼
 1 /**
 2  * 
 3  * @ClassName:MagicHashCode
 4  * @Description:ThreadLocalMap使用“開放定址法”中最簡單的“線性探測法”解決雜湊衝突問題
 5  * @author diandian.zhang
 6  * @date 2017年12月6日上午10:53:28
 7  */
 8 public class MagicHashCode {
 9     //ThreadLocal中定義的hash魔數
10     private static final int HASH_INCREMENT = 0x61c88647;
11     
12     public static void main(String[] args) {
13         hashCode(16);//初始化16
14         hashCode(32);//後續2倍擴容
15         hashCode(64);
16     }
17 
18     /**
19      * 
20      * @Description 尋找雜湊下標(對應陣列小標)
21      * @param length table長度
22      * @author diandian.zhang
23      * @date 2017年12月6日上午10:36:53
24      * @since JDK1.8
25      */
26     private static void hashCode(Integer length){
27         int hashCode = 0; 
28         for(int i=0;i<length;i++){
29             hashCode = i*HASH_INCREMENT+HASH_INCREMENT;//每次遞增HASH_INCREMENT
30             System.out.print(hashCode & (length-1));//求雜湊下標,演算法公式
31             System.out.print(" ");
32         }
33         System.out.println();
34     }
35 }
複製程式碼

執行結果:

7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 --》Entry[]初始化容量為16時,元素完美雜湊  
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0--》Entry[]容量擴容2倍=32時,元素完美雜湊
7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0 --》Entry[]容量擴容2倍=64時,元素完美雜湊

根據執行結果,代表此演算法在長度為2的N次方的陣列上,確實可以完美雜湊

那麼原理是什麼?

long l1 = (long) ((1L << 31) * (Math.sqrt(5) - 1));//(根號5-1)*2的31次方=(根號5-1)/2 *2的32次方=黃金分割數*2的32次方
System.out.println("as 32 bit unsigned: " + l1);//32位無符號整數
int i1 = (int) l1;
System.out.println("as 32 bit signed:   " + i1);//32位有符號整數
System.out.println("MAGIC = " + 0x61c88647);

執行結果:

as 32 bit unsigned: 2654435769
as 32 bit signed:   -1640531527
MAGIC = 1640531527

這裡不再拓展,跟斐波那契數列(和黃金分割數)有關:

1.0x61c88647對應十進位制=1640531527。

2.(根號5-1)*2的31次方,轉換成long型別就是2654435769,轉換成int型別就是-1640531527。

set操作

ThreadLocal的set最終呼叫了ThreadLocalMap的set方法,如下圖

複製程式碼
 1  private void set(ThreadLocal<?> key, Object value) {
 8             Entry[] tab = table;
 9             int len = tab.length;
10             int i = key.threadLocalHashCode & (len-1);// 根據雜湊碼和陣列長度求元素放置的位置,即陣列下標
11             //從i開始往後一直遍歷到陣列最後一個Entry
12             for (Entry e = tab[i];
13                  e != null;
14                  e = tab[i = nextIndex(i, len)]) {
15                 ThreadLocal<?> k = e.get();
16                 //如果key相等,覆蓋value
17                 if (k == key) {
18                     e.value = value;
19                     return;
20                 }
21                 //如果key為null,用新key、value覆蓋,同時清理歷史key=null的陳舊資料
22                 if (k == null) {
23                     replaceStaleEntry(key, value, i);
24                     return;
25                 }
26             }
27 
28             tab[i] = new Entry(key, value);
29             int sz = ++size;
//如果超過閥值,就需要再雜湊了
30 if (!cleanSomeSlots(i, sz) && sz >= threshold) 31 rehash(); 32 }
複製程式碼

再雜湊:

複製程式碼
 1      private void rehash() {
 2             expungeStaleEntries();// 清理一次陳舊資料
 3 
 4             // 清理完陳舊資料,如果>= 3/4閥值,就執行擴容,避免遲滯
 5             if (size >= threshold - threshold / 4)
 6                 resize();
 7         }
 8 
 9         /**
10          * 把table擴容2倍,並把老資料重新雜湊雜湊進新table
11          */
12         private void resize() {
13             Entry[] oldTab = table;
14             int oldLen = oldTab.length;
15             int newLen = oldLen * 2;
16             Entry[] newTab = new Entry[newLen];
17             int count = 0;
18             // 遍歷Entry[]陣列
19             for (int j = 0; j < oldLen; ++j) {
20                 Entry e = oldTab[j];
21                 if (e != null) {
22                     ThreadLocal<?> k = e.get();
23                     if (k == null) {// 如果key=null
24                         e.value = null; // 把value也置null,有助於GC回收物件
25                     } else {// 如果key!=null
26                         int h = k.threadLocalHashCode & (newLen - 1);// 計算hash值 
27                         while (newTab[h] != null)// 如果這個位置已使用
28                             h = nextIndex(h, newLen);// 線性往後查詢,直到找到一個沒有使用的位置,h遞增
29 newTab[h] = e;//在第一個空節點上塞入Entry e 30 count++;// 計數++ 31 } 32 } 33 } 34 35 setThreshold(newLen);// 設定新的閾值(實際set方法用了2/3的newLen作為閾值) 36 size = count;// 設定ThreadLocalMap的元素個數 37 table = newTab;// 把新table賦值給ThreadLocalMap的Entry[] table 38 } 39 40 /** 41 * 刪除陳舊的資料 42 */ 43 private void expungeStaleEntries() { 44 Entry[] tab = table; 45 int len = tab.length; 46 for (int j = 0; j < len; j++) { 47 Entry e = tab[j]; 48 if (e != null && e.get() == null)//entry不為空且entry的key為null 49 expungeStaleEntry(j);//刪除指定陣列下標的陳舊entry 50 } 51 } 52 //刪除陳舊entry的核心方法 53 private int expungeStaleEntry(int staleSlot) { 54 Entry[] tab = table; 55 int len = tab.length; 56 57 58 tab[staleSlot].value = null;//刪除value 59 tab[staleSlot] = null;//刪除entry 60 size--;//map的size自減 61 62 // 遍歷指定刪除節點,所有後續節點 63 Entry e; 64 int i; 65 for (i = nextIndex(staleSlot, len); 66 (e = tab[i]) != null; 67 i = nextIndex(i, len)) { 68 ThreadLocal<?> k = e.get(); 69 if (k == null) {//key為null,執行刪除操作 70 e.value = null; 71 tab[i] = null; 72 size--; 73 } else {//key不為null,重新計算下標 74 int h = k.threadLocalHashCode & (len - 1); 75 if (h != i) {//如果不在同一個位置 76 tab[i] = null;//把老位置的entry置null(刪除) 77 78 // 從h開始往後遍歷,一直到找到空為止,插入 80 while (tab[h] != null) 81 h = nextIndex(h, len); 82 tab[h] = e; 83 } 84 } 85 } 86 return i; 87 }
複製程式碼

總結set步驟:

1)根據雜湊碼和陣列長度求元素放置的位置,即陣列下標

2)從第一步得出的下標開始往後遍歷,如果key相等,覆蓋value,如果key為null,用新key、value覆蓋,同時清理歷史key=null的陳舊資料

3)如果超過閥值,就需要再雜湊:

  • 清理一遍陳舊資料 
  • >= 3/4閥值,就執行擴容,把table擴容2倍==》注意這裡3/4閥值就執行擴容,避免遲滯
  • 把老資料重新雜湊雜湊進新table

 get操作

複製程式碼
 1   public T get() {
 2         Thread t = Thread.currentThread();
 3         ThreadLocalMap map = getMap(t);//從當前執行緒中獲取ThreadLocalMap
 4         if (map != null) {
 5             ThreadLocalMap.Entry e = map.getEntry(this);//查詢當前ThreadLocal變數例項對應的Entry
 6             if (e != null) {//如果不為null,獲取value,返回
 7                 @SuppressWarnings("unchecked")
 8                 T result = (T)e.value;
 9                 return result;
10             }
11         }//如果map為null,即還沒有初始化,走初始化方法
12         return setInitialValue();
13     }
14 
21     private T setInitialValue() {
22         T value = initialValue();//該方法預設返回null,使用者可自定義
23         Thread t = Thread.currentThread();
24         ThreadLocalMap map = getMap(t);
25         if (map != null)//如果map不為null,把初始化value設定進去
26             map.set(this, value);
27         else//如果map為null,則new一個map,並把初始化value設定進去
28             createMap(t, value);
29         return value;
30     }
31 
32     void createMap(Thread t, T firstValue) {
33         t.threadLocals = new ThreadLocalMap(this, firstValue);
34     }
35 
36     ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
37     table = new Entry[INITIAL_CAPACITY];//初始化容量16
38     int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
39     table[i] = new Entry(firstKey, firstValue);
40     size = 1;
41     setThreshold(INITIAL_CAPACITY);//設定閾值
42     }
43     //閾值設定為容量的*2/3,即負載因子為2/3,超過就進行再雜湊
44     private void setThreshold(int len) {
45         threshold = len * 2 / 3;
46      }
複製程式碼

總結get步驟:

1)從當前執行緒中獲取ThreadLocalMap,查詢當前ThreadLocal變數例項對應的Entry,如果不為null,獲取value,返回

2)如果map為null,即還沒有初始化,走初始化方法

remove操作

複製程式碼
 1 public void remove() {
 2     ThreadLocalMap m = getMap(Thread.currentThread());
 3     if (m != null)
 4         m.remove(this);//呼叫ThreadLocalMap刪除變數
 5 }
 6 
 7 private void remove(ThreadLocal<?> key) {
 8     Entry[] tab = table;
 9     int len = tab.length;
10     int i = key.threadLocalHashCode & (len-1);
11     for (Entry e = tab[i];
12          e != null;
13          e = tab[i = nextIndex(i, len)]) {
14         if (e.get() == key) {
15             e.clear();//呼叫Entry的clear方法
16             expungeStaleEntry(i);//清除陳舊資料
17             return;
18         }
19     }
20 }
複製程式碼

看一下Entry的clear方法,Entry ==extends==》 WeakReference<ThreadLocal<?>>==extends==》 Reference<T>,clear方法是抽象類Reference定義的方法。

複製程式碼
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 }
複製程式碼
追一下clear方法如下:把弱引用的物件置null。有利於GC回收記憶體。關於引用,預留飛機票
public void clear() {
    this.referent = null;
}

1.3 功能測試

開啟2個執行緒,每個個執行緒都使用類級別的threadLocal,往裡面遞增數字,i=0,時,set(0),i=1,2,3時 值+1,

複製程式碼
 1 /**
 2  * 
 3  * @ClassName:MyThreadLocal
 4  * @Description:ThreadLocal執行緒本地變數
 5  * @author diandian.zhang
 6  * @date 2017年12月4日上午9:40:52
 7  */
 8 public class MyThreadLocal{
 9     //執行緒本地共享變數
10     private static final ThreadLocal<Object> threadLocal = new ThreadLocal<Object>(){
11         /**
12          * ThreadLocal沒有被當前執行緒賦值時或當前執行緒剛呼叫remove方法後呼叫get方法,返回此方法值
13          */
14         @Override
15         protected Object initialValue()
16         {
17             System.out.println("[執行緒"+Thread.currentThread().getName()+"]呼叫get方法時,當前執行緒共享變數沒值,呼叫initialValue獲取預設值!");
18             return null;
19         }
20     };
21      
22     public static void main(String[] args){
23         //1.開啟任務1執行緒
24         new Thread(new MyIntegerTask("IntegerTask1")).start();
25         //2.中間休息3秒,用以測試資料差異
26         try {
27             Thread.sleep(3000);
28         } catch (InterruptedException e) {
29             e.printStackTrace();
30         }
31         //3.開啟任務2執行緒
32         new Thread(new MyIntegerTask("IntegerTask2")).start();
33     }
34      
35     /**
36      * 
37      * @ClassName:MyIntegerTask
38      * @Description:整形遞增執行緒
39      * @author diandian.zhang
40      * @date 2017年12月4日上午10:00:41
41      */
42     public static class MyIntegerTask implements Runnable{
43         private String name;
44          
45         MyIntegerTask(String name)
46         {
47             this.name = name;
48         }
49  
50         @Override
51         public void run()
52         {
53