Java中的ThreadLocal詳解

風沙迷了眼發表於2019-06-14

一、ThreadLocal簡介

  多執行緒訪問同一個共享變數的時候容易出現併發問題,特別是多個執行緒對一個變數進行寫入的時候,為了保證執行緒安全,一般使用者在訪問共享變數的時候需要進行額外的同步措施才能保證執行緒安全性。ThreadLocal是除了加鎖這種同步方式之外的一種保證一種規避多執行緒訪問出現執行緒不安全的方法,當我們在建立一個變數後,如果每個執行緒對其進行訪問的時候訪問的都是執行緒自己的變數這樣就不會存線上程不安全問題。

  ThreadLocal是JDK包提供的,它提供執行緒本地變數,如果建立一樂ThreadLocal變數,那麼訪問這個變數的每個執行緒都會有這個變數的一個副本,在實際多執行緒操作的時候,操作的是自己本地記憶體中的變數,從而規避了執行緒安全問題,如下圖所示

二、ThreadLocal簡單使用

  下面的例子中,開啟兩個執行緒,在每個執行緒內部設定了本地變數的值,然後呼叫print方法列印當前本地變數的值。如果在列印之後呼叫本地變數的remove方法會刪除本地記憶體中的變數,程式碼如下所示

 1 package test;
 2 
 3 public class ThreadLocalTest {
 4 
 5     static ThreadLocal<String> localVar = new ThreadLocal<>();
 6 
 7     static void print(String str) {
 8         //列印當前執行緒中本地記憶體中本地變數的值
 9         System.out.println(str + " :" + localVar.get());
10         //清除本地記憶體中的本地變數
11         localVar.remove();
12     }
13 
14     public static void main(String[] args) {
15         Thread t1  = new Thread(new Runnable() {
16             @Override
17             public void run() {
18                 //設定執行緒1中本地變數的值
19                 localVar.set("localVar1");
20                 //呼叫列印方法
21                 print("thread1");
22                 //列印本地變數
23                 System.out.println("after remove : " + localVar.get());
24             }
25         });
26 
27         Thread t2  = new Thread(new Runnable() {
28             @Override
29             public void run() {
30                 //設定執行緒1中本地變數的值
31                 localVar.set("localVar2");
32                 //呼叫列印方法
33                 print("thread2");
34                 //列印本地變數
35                 System.out.println("after remove : " + localVar.get());
36             }
37         });
38 
39         t1.start();
40         t2.start();
41     }
42 }

 下面是執行後的結果:

三、ThreadLocal的實現原理

  下面是ThreadLocal的類圖結構,從圖中可知:Thread類中有兩個變數threadLocals和inheritableThreadLocals,二者都是ThreadLocal內部類ThreadLocalMap型別的變數,我們通過檢視內部內ThreadLocalMap可以發現實際上它類似於一個HashMap。在預設情況下,每個執行緒中的這兩個變數都為null,只有當執行緒第一次呼叫ThreadLocal的set或者get方法的時候才會建立他們(後面我們會檢視這兩個方法的原始碼)。除此之外,和我所想的不同的是,每個執行緒的本地變數不是存放在ThreadLocal例項中,而是放在呼叫執行緒的ThreadLocals變數裡面(前面也說過,該變數是Thread類的變數)。也就是說,ThreadLocal型別的本地變數是存放在具體的執行緒空間上,其本身相當於一個裝載本地變數的工具殼,通過set方法將value新增到呼叫執行緒的threadLocals中,當呼叫執行緒呼叫get方法時候能夠從它的threadLocals中取出變數。如果呼叫執行緒一直不終止,那麼這個本地變數將會一直存放在他的threadLocals中,所以不使用本地變數的時候需要呼叫remove方法將threadLocals中刪除不用的本地變數。下面我們通過檢視ThreadLocal的set、get以及remove方法來檢視ThreadLocal具體實怎樣工作的

  1、set方法原始碼

 1 public void set(T value) {
 2     //(1)獲取當前執行緒(呼叫者執行緒)
 3     Thread t = Thread.currentThread();
 4     //(2)以當前執行緒作為key值,去查詢對應的執行緒變數,找到對應的map
 5     ThreadLocalMap map = getMap(t);
 6     //(3)如果map不為null,就直接新增本地變數,key為當前執行緒,值為新增的本地變數值
 7     if (map != null)
 8         map.set(this, value);
 9     //(4)如果map為null,說明首次新增,需要首先建立出對應的map
10     else
11         createMap(t, value);
12 }

  在上面的程式碼中,(2)處呼叫getMap方法獲得當前執行緒對應的threadLocals(參照上面的圖示和文字說明),該方法程式碼如下

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals; //獲取執行緒自己的變數threadLocals,並繫結到當前呼叫執行緒的成員變數threadLocals上
}

  如果呼叫getMap方法返回值不為null,就直接將value值設定到threadLocals中(key為當前執行緒引用,值為本地變數);如果getMap方法返回null說明是第一次呼叫set方法(前面說到過,threadLocals預設值為null,只有呼叫set方法的時候才會建立map),這個時候就需要呼叫createMap方法建立threadLocals,該方法如下所示

1 void createMap(Thread t, T firstValue) {
2     t.threadLocals = new ThreadLocalMap(this, firstValue);
3 }

  createMap方法不僅建立了threadLocals,同時也將要新增的本地變數值新增到了threadLocals中。

  2、get方法原始碼

  在get方法的實現中,首先獲取當前呼叫者執行緒,如果當前執行緒的threadLocals不為null,就直接返回當前執行緒繫結的本地變數值,否則執行setInitialValue方法初始化threadLocals變數。在setInitialValue方法中,類似於set方法的實現,都是判斷當前執行緒的threadLocals變數是否為null,是則新增本地變數(這個時候由於是初始化,所以新增的值為null),否則建立threadLocals變數,同樣新增的值為null。

 1 public T get() {
 2     //(1)獲取當前執行緒
 3     Thread t = Thread.currentThread();
 4     //(2)獲取當前執行緒的threadLocals變數
 5     ThreadLocalMap map = getMap(t);
 6     //(3)如果threadLocals變數不為null,就可以在map中查詢到本地變數的值
 7     if (map != null) {
 8         ThreadLocalMap.Entry e = map.getEntry(this);
 9         if (e != null) {
10             @SuppressWarnings("unchecked")
11             T result = (T)e.value;
12             return result;
13         }
14     }
15     //(4)執行到此處,threadLocals為null,呼叫該更改初始化當前執行緒的threadLocals變數
16     return setInitialValue();
17 }
18 
19 private T setInitialValue() {
20     //protected T initialValue() {return null;}
21     T value = initialValue();
22     //獲取當前執行緒
23     Thread t = Thread.currentThread();
24     //以當前執行緒作為key值,去查詢對應的執行緒變數,找到對應的map
25     ThreadLocalMap map = getMap(t);
26     //如果map不為null,就直接新增本地變數,key為當前執行緒,值為新增的本地變數值
27     if (map != null)
28         map.set(this, value);
29     //如果map為null,說明首次新增,需要首先建立出對應的map
30     else
31         createMap(t, value);
32     return value;
33 }

  3、remove方法的實現

  remove方法判斷該當前執行緒對應的threadLocals變數是否為null,不為null就直接刪除當前執行緒中指定的threadLocals變數

1  public void remove() {
2     //獲取當前執行緒繫結的threadLocals
3      ThreadLocalMap m = getMap(Thread.currentThread());
4      //如果map不為null,就移除當前執行緒中指定ThreadLocal例項的本地變數
5      if (m != null)
6          m.remove(this);
7  }

  4、如下圖所示:每個執行緒內部有一個名為threadLocals的成員變數,該變數的型別為ThreadLocal.ThreadLocalMap型別(類似於一個HashMap),其中的key為當前定義的ThreadLocal變數的this引用,value為我們使用set方法設定的值。每個執行緒的本地變數存放在自己的本地記憶體變數threadLocals中,如果當前執行緒一直不消亡,那麼這些本地變數就會一直存在(所以可能會導致記憶體溢位),因此使用完畢需要將其remove掉。

四、ThreadLocal不支援繼承性

  同一個ThreadLocal變數在父執行緒中被設定值後,在子執行緒中是獲取不到的。(threadLocals中為當前呼叫執行緒對應的本地變數,所以二者自然是不能共享的)

 1 package test;
 2 
 3 public class ThreadLocalTest2 {
 4 
 5     //(1)建立ThreadLocal變數
 6     public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
 7 
 8     public static void main(String[] args) {
 9         //在main執行緒中新增main執行緒的本地變數
10         threadLocal.set("mainVal");
11         //新建立一個子執行緒
12         Thread thread = new Thread(new Runnable() {
13             @Override
14             public void run() {
15                 System.out.println("子執行緒中的本地變數值:"+threadLocal.get());
16             }
17         });
18         thread.start();
19         //輸出main執行緒中的本地變數值
20         System.out.println("mainx執行緒中的本地變數值:"+threadLocal.get());
21     }
22 }

五、InheritableThreadLocal類

  在上面說到的ThreadLocal類是不能提供子執行緒訪問父執行緒的本地變數的,而InheritableThreadLocal類則可以做到這個功能,下面是該類的原始碼

 1 public class InheritableThreadLocal<T> extends ThreadLocal<T> {
 2     
 3     protected T childValue(T parentValue) {
 4         return parentValue;
 5     }
 6 
 7     ThreadLocalMap getMap(Thread t) {
 8        return t.inheritableThreadLocals;
 9     }
10 
11     void createMap(Thread t, T firstValue) {
12         t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
13     }
14 }

  從上面程式碼可以看出,InheritableThreadLocal類繼承了ThreadLocal類,並重寫了childValue、getMap、createMap三個方法。其中createMap方法在被呼叫(當前執行緒呼叫set方法時得到的map為null的時候需要呼叫該方法)的時候,建立的是inheritableThreadLocal而不是threadLocals。同理,getMap方法在當前呼叫者執行緒呼叫get方法的時候返回的也不是threadLocals而是inheritableThreadLocal。

  下面我們看看重寫的childValue方法在什麼時候執行,怎樣讓子執行緒訪問父執行緒的本地變數值。我們首先從Thread類開始說起

 1 private void init(ThreadGroup g, Runnable target, String name,
 2                   long stackSize) {
 3     init(g, target, name, stackSize, null, true);
 4 }
 5 private void init(ThreadGroup g, Runnable target, String name,
 6                   long stackSize, AccessControlContext acc,
 7                   boolean inheritThreadLocals) {
 8     //判斷名字的合法性
 9     if (name == null) {
10         throw new NullPointerException("name cannot be null");
11     }
12 
13     this.name = name;
14     //(1)獲取當前執行緒(父執行緒)
15     Thread parent = currentThread();
16     //安全校驗
17     SecurityManager security = System.getSecurityManager();
18     if (g == null) { //g:當前執行緒組
19         if (security != null) {
20             g = security.getThreadGroup();
21         }
22         if (g == null) {
23             g = parent.getThreadGroup();
24         }
25     }
26     g.checkAccess();
27     if (security != null) {
28         if (isCCLOverridden(getClass())) {
29             security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
30         }
31     }
32 
33     g.addUnstarted();
34 
35     this.group = g; //設定為當前執行緒組
36     this.daemon = parent.isDaemon();//守護執行緒與否(同父執行緒)
37     this.priority = parent.getPriority();//優先順序同父執行緒
38     if (security == null || isCCLOverridden(parent.getClass()))
39         this.contextClassLoader = parent.getContextClassLoader();
40     else
41         this.contextClassLoader = parent.contextClassLoader;
42     this.inheritedAccessControlContext =
43             acc != null ? acc : AccessController.getContext();
44     this.target = target;
45     setPriority(priority);
46     //(2)如果父執行緒的inheritableThreadLocal不為null
47     if (inheritThreadLocals && parent.inheritableThreadLocals != null)
48         //(3)設定子執行緒中的inheritableThreadLocals為父執行緒的inheritableThreadLocals
49         this.inheritableThreadLocals =
50             ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
51     this.stackSize = stackSize;
52 
53     tid = nextThreadID();
54 }

  在init方法中,首先(1)處獲取了當前執行緒(父執行緒),然後(2)處判斷當前父執行緒的inheritableThreadLocals是否為null,然後呼叫createInheritedMap將父執行緒的inheritableThreadLocals作為建構函式引數建立了一個新的ThreadLocalMap變數,然後賦值給子執行緒。下面是createInheritedMap方法和ThreadLocalMap的構造方法

 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                 //呼叫重寫的方法
18                 Object value = key.childValue(e.value);
19                 Entry c = new Entry(key, value);
20                 int h = key.threadLocalHashCode & (len - 1);
21                 while (table[h] != null)
22                     h = nextIndex(h, len);
23                 table[h] = c;
24                 size++;
25             }
26         }
27     }
28 }

  在建構函式中將父執行緒的inheritableThreadLocals成員變數的值賦值到新的ThreadLocalMap物件中。返回之後賦值給子執行緒的inheritableThreadLocals。總之,InheritableThreadLocals類通過重寫getMap和createMap兩個方法將本地變數儲存到了具體執行緒的inheritableThreadLocals變數中,當執行緒通過InheritableThreadLocals例項的set或者get方法設定變數的時候,就會建立當前執行緒的inheritableThreadLocals變數。而父執行緒建立子執行緒的時候,ThreadLocalMap中的建構函式會將父執行緒的inheritableThreadLocals中的變數複製一份到子執行緒的inheritableThreadLocals變數中。

六、從ThreadLocalMap看ThreadLocal使用不當的記憶體洩漏問題

1、基礎概念 

  首先我們先看看ThreadLocalMap的類圖,在前面的介紹中,我們知道ThreadLocal只是一個工具類,他為使用者提供get、set、remove介面操作實際存放本地變數的threadLocals(呼叫執行緒的成員變數),也知道threadLocals是一個ThreadLocalMap型別的變數,下面我們來看看ThreadLocalMap這個類。在此之前,我們回憶一下Java中的四種引用型別,相關GC只是參考前面系列的文章(JVM相關)

①強引用:Java中預設的引用型別,一個物件如果具有強引用那麼只要這種引用還存在就不會被GC。

②軟引用:簡言之,如果一個物件具有弱引用,在JVM發生OOM之前(即記憶體充足夠使用),是不會GC這個物件的;只有到JVM記憶體不足的時候才會GC掉這個物件。軟引用和一個引用佇列聯合使用,如果軟引用所引用的物件被回收之後,該引用就會加入到與之關聯的引用佇列中

③弱引用(這裡討論ThreadLocalMap中的Entry類的重點):如果一個物件只具有弱引用,那麼這個物件就會被垃圾回收器GC掉(被弱引用所引用的物件只能生存到下一次GC之前,當發生GC時候,無論當前記憶體是否足夠,弱引用所引用的物件都會被回收掉)。弱引用也是和一個引用佇列聯合使用,如果弱引用的物件被垃圾回收期回收掉,JVM會將這個引用加入到與之關聯的引用佇列中。若引用的物件可以通過弱引用的get方法得到,當引用的物件唄回收掉之後,再呼叫get方法就會返回null

④虛引用:虛引用是所有引用中最弱的一種引用,其存在就是為了將關聯虛引用的物件在被GC掉之後收到一個通知。(不能通過get方法獲得其指向的物件)

2、分析ThreadLocalMap內部實現

  上面我們知道ThreadLocalMap內部實際上是一個Entry陣列,我們先看看Entry的這個內部類

 1 /**
 2  * 是繼承自WeakReference的一個類,該類中實際存放的key是
 3  * 指向ThreadLocal的弱引用和與之對應的value值(該value值
 4  * 就是通過ThreadLocal的set方法傳遞過來的值)
 5  * 由於是弱引用,當get方法返回null的時候意味著坑能引用
 6  */
 7 static class Entry extends WeakReference<ThreadLocal<?>> {
 8     /** value就是和ThreadLocal繫結的 */
 9     Object value;
10 
11     //k:ThreadLocal的引用,被傳遞給WeakReference的構造方法
12     Entry(ThreadLocal<?> k, Object v) {
13         super(k);
14         value = v;
15     }
16 }
17 //WeakReference構造方法(public class WeakReference<T> extends Reference<T> )
18 public WeakReference(T referent) {
19     super(referent); //referent:ThreadLocal的引用
20 }
21 
22 //Reference構造方法     
23 Reference(T referent) {
24     this(referent, null);//referent:ThreadLocal的引用
25 }
26 
27 Reference(T referent, ReferenceQueue<? super T> queue) {
28     this.referent = referent;
29     this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
30 }

  在上面的程式碼中,我們可以看出,當前ThreadLocal的引用k被傳遞給WeakReference的建構函式,所以ThreadLocalMap中的key為ThreadLocal的弱引用。當一個執行緒呼叫ThreadLocal的set方法設定變數的時候,當前執行緒的ThreadLocalMap就會存放一個記錄,這個記錄的key值為ThreadLocal的弱引用,value就是通過set設定的值。如果當前執行緒一直存在且沒有呼叫該ThreadLocal的remove方法,如果這個時候別的地方還有對ThreadLocal的引用,那麼當前執行緒中的ThreadLocalMap中會存在對ThreadLocal變數的引用和value物件的引用,是不會釋放的,就會造成記憶體洩漏。

  考慮這個ThreadLocal變數沒有其他強依賴,如果當前執行緒還存在,由於執行緒的ThreadLocalMap裡面的key是弱引用,所以當前執行緒的ThreadLocalMap裡面的ThreadLocal變數的弱引用在gc的時候就被回收,但是對應的value還是存在的這就可能造成記憶體洩漏(因為這個時候ThreadLocalMap會存在key為null但是value不為null的entry項)。

  總結:THreadLocalMap中的Entry的key使用的是ThreadLocal物件的弱引用,在沒有其他地方對ThreadLoca依賴,ThreadLocalMap中的ThreadLocal物件就會被回收掉,但是對應的不會被回收,這個時候Map中就可能存在key為null但是value不為null的項,這需要實際的時候使用完畢及時呼叫remove方法避免記憶體洩漏。

相關文章