1. 程式人生 > >ThreadLocal 類 的源碼解析以及使用原理

ThreadLocal 類 的源碼解析以及使用原理

init top assign 執行 第一次 利用 reat 有助於 單獨

1、原理圖說明

  技術分享圖片

  首先看這一張圖,我們可以看出,每一個Thread類中都存在一個屬性 ThreadLocalMap 成員,該成員是一個map數據結構,map中是一個Entry的數組,存在entry實體,該實體包含了 key value hash (註意 此map結構不包含next引用 所以不是使用的鏈地址方法)。

可以是用來存放 ThreadLocal對象以及對應的變量副本;

根據這個原理。我們可以知道在一個線程中可以存儲多個 ThreadLocal 對象以及對應的value副本; 所以ThreadLocal 對象的作用就是用來為每一個線程 維護一個 副本;

  我們使用ThreadLocal解決線程局部變量統一定義問題,

多線程數據不能共享。(InheritableThreadLocal特例除外)不能解決並發問題。解決了:基於類級別的變量定義,每一個線程單獨維護自己線程內的變量值(存、取、刪的功能

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

技術分享圖片

2、源碼分析

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源碼註釋

下圖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) {
          //Entry 為 ThreadLocal 的靜態內部類 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); //註意次數的this 指的是ThreadLocal對象 也就是說 entry中的鍵是 ThreadLocal 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);//相當於找到 鍵 後 刪除掉整個Entry 實體 65 } 66 74 ThreadLocalMap getMap(Thread t) { 75 return t.threadLocals; 76 }
技術分享圖片 技術分享圖片

看源碼我們知道不管是set、get、remove操作的都是ThreadLocalMap,key=ThreadLocal ,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),//為什麽這樣計算 詳細看我的另一篇博客 hashmap的原理總結

技術分享圖片

下面校驗算法的散列性:

技術分享圖片 技術分享圖片
 1 /**
 2  * 
 3  * @ClassName:MagicHashCode
 4  * @Description:ThreadLocalMap使用“開放尋址法”中最簡單的“線性探測法”解決散列沖突問題
 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]; e != null; 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();// 清理一次陳舊數據  //保證數據及時 GC
 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             for(int i = 0; i < 5; i++)
54             {
55                 // ThreadLocal.get方法獲取線程變量
56                 if(null == MyThreadLocal.threadLocal.get())
57                 {
58                     // ThreadLocal.set方法設置線程變量
59                     MyThreadLocal.threadLocal.set(0);
60                     System.out.println("i="+i+"[線程" + name + "]當前線程不存在緩存,set 0");
61                 }
62                 else
63                 {
64                     int num = (Integer)MyThreadLocal.threadLocal.get();
65                     MyThreadLocal.threadLocal.set(num + 1);
66                     System.out.println("i="+i+"[線程" + name + "]往threadLocal中set: " + MyThreadLocal.threadLocal.get());
67                     //當i=3即循環4次時,移除當前線程key
68                     if(i == 3)
69                     {
70                         System.out.println("i="+i+"[線程" + name + "],threadLocal移除當前線程" );
71                         MyThreadLocal.threadLocal.remove();
72                     }
73                 }
74                 try
75                 {
76                     Thread.sleep(1000);
77                 }
78                 catch (InterruptedException e)
79                 {
80                     e.printStackTrace();
81                 }
82             }  
83         }
84     }
85 }
技術分享圖片 技術分享圖片

運行結果如下:

技術分享圖片 技術分享圖片
[線程Thread-0]調用get方法時,當前線程共享變量沒值,調用initialValue獲取默認值!
i=0[線程IntegerTask1]當前線程不存在緩存,set 0
i=1[線程IntegerTask1]往threadLocal中set: 1
i=2[線程IntegerTask1]往threadLocal中set: 2
[線程Thread-1]調用get方法時,當前線程共享變量沒值,調用initialValue獲取默認值!
i=0[線程IntegerTask2]當前線程不存在緩存,set 0
i=3[線程IntegerTask1]往threadLocal中set: 3
i=3[線程IntegerTask1],threadLocal移除當前線程
i=1[線程IntegerTask2]往threadLocal中set: 1
[線程Thread-0]調用get方法時,當前線程共享變量沒值,調用initialValue獲取默認值!
i=4[線程IntegerTask1]當前線程不存在緩存,set 0
i=2[線程IntegerTask2]往threadLocal中set: 2
i=3[線程IntegerTask2]往threadLocal中set: 3
i=3[線程IntegerTask2],threadLocal移除當前線程
[線程Thread-1]調用get方法時,當前線程共享變量沒值,調用initialValue獲取默認值!
i=4[線程IntegerTask2]當前線程不存在緩存,set 0
技術分享圖片 技術分享圖片

結果驗證:

1.2個線程,2個threadLocal變量互不影響。

2.調用get方法時,對應ThreadLocalMap為null會調用initialValue()方法,初始化threadLocal的值。

1.4 應用場景

ThreadLocal的實際應用場景:

1)數據結構:用Map<String, Object>來避免創建多個ThreadLocal變量的麻煩。只需根據map的key就可以獲取想要的value

private static final ThreadLocal<Map<String, Object>> loginContext = new ThreadLocal<>();

2)業務:線程級別,維護session,維護用戶登錄信息userID(登陸時插入,多個地方獲取)等,尤其適合使用在WEB項目中(Tomcat容器,工作線程隔離)

二、變量可繼承的ThreadLocal==》InheritableThreadLocal

2.1 源碼註釋:

這個類擴展ThreadLocal,以提供從父線程到子線程的值的繼承:當創建子線程時,子線程會接收父元素所具有值的所有可繼承線程局部變量的初始值。正常情況下,子線程的變量值與父線程的相同;然而,子線程可復寫childValue方法來自定義獲取父類變量。
當變量(例如,用戶ID、事務ID)中維護的每個線程屬性必須自動傳輸到創建的任何子線程時,使用InheritableThreadLocal優於ThreadLocal。

2.2 源碼剖析

1.子線程啟動時,調用init方法,如果父線程有InheritableThreadLocal變量,則在子線程也生成一份

下圖是Thread類在init時執行的邏輯:

技術分享圖片

調用createInheritedMap方法,並調用childValue方法復制一份變量給子線程



技術分享圖片
技術分享圖片
 1 static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
 2         return new ThreadLocalMap(parentMap);
 3     }
 4 
 5 private ThreadLocalMap(ThreadLocalMap parentMap) {
 6             Entry[] parentTable = parentMap.table;
 7             int len = parentTable.length;
 8             setThreshold(len);
 9             table = new Entry[len];
10 
11             for (int j = 0; j < len; j++) {
12                 Entry e = parentTable[j];
13                 if (e != null) {
14                     @SuppressWarnings("unchecked")
15                     ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
16                     if (key != null) {
17                         Object value = key.childValue(e.value);
18                         Entry c = new Entry(key, value);
19                         int h = key.threadLocalHashCode & (len - 1);
20                         while (table[h] != null)
21                             h = nextIndex(h, len);
22                         table[h] = c;
23                         size++;
24                     }
25                 }
26             }
27         }
技術分享圖片 技術分享圖片

2.支持用戶自定義childValue函數,用以子類獲取父類變量值的轉換:父類變量----childValue轉換函數-----》子類變量

InheritableThreadLocal默認childValue函數是直接返回:

protected T childValue(T parentValue) {
    return parentValue;
}

用戶可在創建InheritableThreadLocal變量時,覆蓋childValue函數,見3.3測試

2.3 功能測試

技術分享圖片 技術分享圖片
 1 package threadLocal;
 2 
 3 
 4 /**
 5  * 
 6  * @ClassName:MyInheritableThreadLocal
 7  * @Description:可繼承線程本地變量
 8  * @author denny.zhang
 9  * @date 2017年12月7日下午5:24:40
10  */
11 public class MyInheritableThreadLocal{
12     //線程本地共享變量
13     private static final InheritableThreadLocal<Object> threadLocal = new InheritableThreadLocal<Object>(){
14         /**
15          * ThreadLocal沒有被當前線程賦值時或當前線程剛調用remove方法後調用get方法,返回此方法值
16          */
17         @Override
18         protected Object initialValue()
19         {
20             System.out.println("[線程"+Thread.currentThread().getName()+"]調用get方法時,當前線程共享變量沒值,調用initialValue獲取默認值!");
21             return null;
22         }
23         
24         @Override
25         protected Object childValue(Object parentValue) {
26             return (Integer)parentValue*2;
27         }
28         
29     };
30      
31     public static void main(String[] args){
32         //主線程設置1
33         threadLocal.set(1);
34         //1.開啟任務1線程
35         new Thread(new MyIntegerTask("IntegerTask1")).start();
36         //2.中間休息3秒,用以測試數據差異
37         try {
38             Thread.sleep(3000);
39         } catch (InterruptedException e) {
40             e.printStackTrace();
41         }
42         //開啟任務2線程
43         new Thread(new MyIntegerTask("IntegerTask2")).start();
44     }
45      
46     /**
47      * 
48      * @ClassName:MyIntegerTask
49      * @Description:整形遞增線程
50      * @author diandian.zhang
51      * @date 2017年12月4日上午10:00:41
52      */
53     public static class MyIntegerTask implements Runnable{
54         private String name;
55          
56         MyIntegerTask(String name)
57         {
58             this.name = name;
59         }
60  
61         @Override
62         public void run()
63         {
64             for(int i = 0; i < 5; i++)
65             {
66                 // ThreadLocal.get方法獲取線程變量
67                 if(null == MyInheritableThreadLocal.threadLocal.get())
68                 {
69                     // ThreadLocal.set方法設置線程變量
70                     MyInheritableThreadLocal.threadLocal.set(0);
71                     System.out.println("i="+i+"[線程" + name + "]當前線程不存在緩存,set 0");
72                 }
73                 else
74                 {
75                     int num = (Integer)MyInheritableThreadLocal.threadLocal.get();
76                     System.out.println("i="+i+"[線程" + name + "]get=" + num);
77                     MyInheritableThreadLocal.threadLocal.set(num + 1);
78                     System.out.println("i="+i+"[線程" + name + "]往threadLocal中set: " + MyInheritableThreadLocal.threadLocal.get());
79                     //當i=3即循環4次時,移除當前線程key
80                     if(i == 3)
81                     {
82                         System.out.println("i="+i+"[線程" + name + "],remove" );
83                         MyInheritableThreadLocal.threadLocal.remove();
84                     }
85                 }
86                 try
87                 {
88                     Thread.sleep(1000);
89                 }
90                 catch (InterruptedException e)
91                 {
92                     e.printStackTrace();
93                 }
94             }  
95         }
96     }
97 }
技術分享圖片 技術分享圖片

運行結果:

技術分享圖片 技術分享圖片
主線程變量值=1-----》主線程中變量值1
i=0[線程IntegerTask1]get=2-----》子線程1中變量值=2*1=2,驗證通過! i=0[線程IntegerTask1]往threadLocal中set: 3 i=1[線程IntegerTask1]get=3 i=1[線程IntegerTask1]往threadLocal中set: 4 i=2[線程IntegerTask1]get=4 i=2[線程IntegerTask1]往threadLocal中set: 5 i=0[線程IntegerTask2]get=2-----》主線程2中變量值=2*1=2,驗證通過! i=0[線程IntegerTask2]往threadLocal中set: 3 i=3[線程IntegerTask1]get=5 i=3[線程IntegerTask1]往threadLocal中set: 6 i=3[線程IntegerTask1],remove i=1[線程IntegerTask2]get=3 i=1[線程IntegerTask2]往threadLocal中set: 4 [線程Thread-0]調用get方法時,當前線程共享變量沒值,調用initialValue獲取默認值! i=4[線程IntegerTask1]當前線程不存在緩存,set 0 i=2[線程IntegerTask2]get=4 i=2[線程IntegerTask2]往threadLocal中set: 5 i=3[線程IntegerTask2]get=5 i=3[線程IntegerTask2]往threadLocal中set: 6 i=3[線程IntegerTask2],remove [線程Thread-1]調用get方法時,當前線程共享變量沒值,調用initialValue獲取默認值! i=4[線程IntegerTask2]當前線程不存在緩存,set 0
技術分享圖片 技術分享圖片

如上圖,分析結果我們可知,

1.子線程根據childValue函數獲取到了父線程的變量值。

2.多線程InheritableThreadLocal變量各自維護,無競爭關系。

2.4 應用場景

子線程變量數據依賴父線程變量,且自定義賦值函數。

例如:

開啟多線程執行任務時,總任務名稱叫mainTask 子任務名稱依次遞增mainTask-subTask1、mainTask-subTask2、mainTask-subTaskN等等

三、總結

本文分析了ThreadLocal原理、set(散列算法原理和測試驗證,再哈希擴容)、get、remove源碼,實際中的應用場景以及功能測試驗證。最後又分析了InheritableThreadLocal,使用該類子線程會繼承父線程變量,並自定義賦值函數。
讀完本文,相信大家對ThreadLocal一點也不擔心了哈哈!

需要註意2點:

1.ThreadLocal不是用來解決線程安全問題的,多線程不共享,不存在競爭!目的是線程本地變量且只能單個線程內維護使用。

2.InheritableThreadLocal對比ThreadLocal唯一不同是子線程會繼承父線程變量,並自定義賦值函數。

3.項目如果使用了線程池,那麽小心線程回收後ThreadLocal、InheritableThreadLocal變量要remove,否則線程池回收後,變量還在內存中,後果不堪設想!(例如Tomcat容器的線程池,可以在攔截器中處理:extends HandlerInterceptorAdapter,然後復寫afterCompletion方法,remove掉變量!!!)

ThreadLocal 類 的源碼解析以及使用原理