早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal為解決多執行緒程式的併發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多執行緒程式。ThreadLocal是指作用域為Thread的區域性變數,也許把它命名為ThreadLocalVariable更容易讓人理解一些。此部落格很多內容參考了(這篇部落格https://www.cnblogs.com/fsmly/p/11020641.html).

介紹

多執行緒訪問同一個共享變數的時候容易出現併發問題,特別是多個執行緒對一個變數進行寫入的時候,為了保證執行緒安全,一般使用者在訪問共享變數的時候需要進行額外的同步措施才能保證執行緒安全性。ThreadLocal是除了加鎖這種同步方式之外的一種保證一種規避多執行緒訪問出現執行緒不安全的方法,當我們在建立一個變數後,如果每個執行緒對其進行訪問的時候訪問的都是執行緒自己的變數這樣就不會存線上程不安全問題。 ThreadLocal是JDK包提供的,它提供執行緒本地變數,如果建立一個ThreadLocal變數,那麼訪問這個變數的每個執行緒都會有這個變數的一個副本,在實際多執行緒操作的時候,操作的是自己本地記憶體中的變數,從而規避了執行緒安全問題,如下圖所示:

ThreadLocal使用示例

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

package test;

public class ThreadLocalTest {

    static ThreadLocal<String> localVar = new ThreadLocal<>();

    static void print(String str) {
//列印當前執行緒中本地記憶體中本地變數的值
System.out.println(str + " :" + localVar.get());
//清除本地記憶體中的本地變數
localVar.remove();
} public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//設定執行緒1中本地變數的值
localVar.set("localVar1");
//呼叫列印方法
print("thread1");
//列印本地變數
System.out.println("after remove : " + localVar.get());
}
}); Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//設定執行緒1中本地變數的值
localVar.set("localVar2");
//呼叫列印方法
print("thread2");
//列印本地變數
System.out.println("after remove : " + localVar.get());
}
}); t1.start();
t2.start();
}
}

ThreadLocal的實現原理

從上一節中我們可以看出,ThreadLocal主要有set和get方法,用於設定和獲取執行緒中的變數,那麼ThreadLocal是怎麼實現這個功能的呢?和ThreadLocal實現相關的類主要有三個:ThreadLocal、Thread、ThreadLocalMap,三者之間的關係同樣如下圖所示:

  1. ThreadLocalMap:名字上看是Map,實際上是一個數組,不過它的功能和Map類似,可以按照key查詢資料。
  2. Thread:執行緒大家應該都知道,那麼在ThreadLocal中他起什麼作用呢?一個Thread中會包含兩個ThreadLocalMap,分別用於儲存本執行緒和父執行緒的ThreadLocal資料。每一個ThreadLocal變數會線上程中對應一條ThreadLocalMap的key-value,其中key是ThreadLocal的唯一Hash值。
  3. ThreadLocal:每個ThreadLocal都會有一個唯一的Hash值,用於查詢這個ThreadLocal在ThreadLocalMap中的值;ThreadLocal提供了方法用於獲取當前執行緒的ThreadLocal資料。

資料存放的位置

ThreadLocal只是一層訪問執行緒資料的殼,ThreadLocal get和set的資料不會在ThreadLocal的例項中存放,而是存放線上程Thread中的ThreadLocalMap,ThreadLocal只是提供了一個訪問這些資料的途徑。

ThreadLoca的set方法將value新增到呼叫執行緒的ThreadLocalMap中,當呼叫執行緒呼叫get方法時候能夠從它的ThreadLocalMap中取出變數。如果呼叫執行緒一直不終止,那麼這個本地變數將會一直存放在他的ThreadLocalMap中,所以不使用本地變數的時候需要呼叫remove方法將ThreadLocalMap中刪除不用的本地變數。

set方法存放資料

ThreadLocal方法的set可以向當前執行緒的ThreadLocalMap中放入資料,存放資料的原始碼如下所示,Set過程分為以下步驟:

  1. 獲取當前執行緒。
  2. 從當前執行緒中獲取ThreadLocalMap變數。
  3. 如果當前執行緒的ThreadLocalMap不為空,用當前的ThreadLocal為Key,需要存放的資料為Value,存放資料。
  4. 如果當前執行緒的ThreadLocalMap為空,建立ThreadLocalMap並存放資料。
public void set(T value) {
//(1)獲取當前執行緒(呼叫者執行緒)
Thread t = Thread.currentThread();
//(2)以當前執行緒作為key值,去查詢對應的執行緒變數,找到對應的map
ThreadLocalMap map = getMap(t);
//(3)如果map不為null,就直接新增本地變數,key為當前定義的ThreadLocal變數的this引用,值為新增的本地變數值
if (map != null)
map.set(this, value);
//(4)如果map為null,說明首次新增,需要首先創建出對應的map
else
createMap(t, value);
}

get方法獲取資料

ThreadLocal方法的get可以獲取當前執行緒ThreadLocalMap中存放的資料,獲取存放資料的原始碼如下所示,get過程分為以下步驟:

  1. 獲取當前執行緒
  2. 從當前執行緒中獲取ThreadLocalMap變數。
  3. 如果ThreadLocalMap變數不為null,就可以在map中查詢到本地變數的值。
  4. 如果ThreadLocalMap變數為null,那麼就初始化當前執行緒的ThreadLocalMap。
public T get() {
//(1)獲取當前執行緒
Thread t = Thread.currentThread();
//(2)獲取當前執行緒的threadLocals變數
ThreadLocalMap map = getMap(t);
//(3)如果threadLocals變數不為null,就可以在map中查詢到本地變數的值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//(4)執行到此處,threadLocals為null,呼叫該更改初始化當前執行緒的threadLocals變數
return setInitialValue();
} private T setInitialValue() {
//protected T initialValue() {return null;}
T value = initialValue();
//獲取當前執行緒
Thread t = Thread.currentThread();
//以當前執行緒作為key值,去查詢對應的執行緒變數,找到對應的map
ThreadLocalMap map = getMap(t);
//如果map不為null,就直接新增本地變數,key為當前執行緒,值為新增的本地變數值
if (map != null)
map.set(this, value);
//如果map為null,說明首次新增,需要首先創建出對應的map
else
createMap(t, value);
return value;
}

ThreadLocal不支援繼承性

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

package test;

public class ThreadLocalTest2 {

    //(1)建立ThreadLocal變數
public static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) {
//在main執行緒中新增main執行緒的本地變數
threadLocal.set("mainVal");
//新建立一個子執行緒
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子執行緒中的本地變數值:"+threadLocal.get());
}
});
thread.start();
//輸出main執行緒中的本地變數值
System.out.println("mainx執行緒中的本地變數值:"+threadLocal.get());
}
}

InheritableThreadLocal類

在上面說到的ThreadLocal類是不能提供子執行緒訪問父執行緒的本地變數的,而InheritableThreadLocal類則可以做到這個功能,下面是該類的原始碼,InheritableThreadLocal類繼承了ThreadLocal類,並重寫了childValue、getMap、createMap三個方法。我們接下來分別介紹一下三種方法的用處。

  1. createMap:當執行緒中不存在ThreadLocalMap變數,但是呼叫set或者get方法設定值的時候,需要初始化ThreadLocalMap變數時呼叫該方法。
  2. getMap:需要獲取執行緒的ThreadLocalMap時呼叫該方法,這裡返回的ThreadLocalMap始終為InheritableThreadLocalMap。
  3. childValue:在建立新執行緒的時候,如果父執行緒有ThreadLocalMap變數並且允許inherite ThreadLocalMap,那麼程式會複製父執行緒的InheritableThreadLocal到子執行緒中,childValue表示在複製過程中如何根據父執行緒中得到資料生成執行緒中的資料。


public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* Creates an inheritable thread local variable.
*/
public InheritableThreadLocal() {} /**
* Computes the child's initial value for this inheritable thread-local
* variable as a function of the parent's value at the time the child
* thread is created. This method is called from within the parent
* thread before the child is started.
* <p>
* This method merely returns its input argument, and should be overridden
* if a different behavior is desired.
*/
protected T childValue(T parentValue) {
return parentValue;
} /**
* Get the map associated with a ThreadLocal.
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
} /**
* Create the map associated with a ThreadLocal.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}

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

ThreadLocal記憶體洩漏

通過前面的分析我們知道,ThreadLocal的執行緒資料是存放在ThreadLocalMap中的,所以如果ThreadLocal出現記憶體洩漏,那麼肯定是ThreadLocalMap中儲存的資料出現了洩露,我們需要看看ThreadLocalMap中的資料結構。ThreadLocalMap的資料結構如下所示,ThreadLocalMap中的資料儲存在一個Entry陣列中,Entry中有對ThreadLocal的WeakReference。

什麼情況下會出現記憶體洩露呢?

  1. 當一個執行緒呼叫ThreadLocal的set方法設定變數的時候,當前執行緒的ThreadLocalMap就會存放一個記錄,這個記錄的key值為ThreadLocal的弱引用,value就是通過set設定的值。
  2. 如果當前執行緒一直存在且沒有呼叫該ThreadLocal的remove方法,如果這個時候別的地方還有對ThreadLocal的引用,那麼當前執行緒中的ThreadLocalMap中會存在對ThreadLocal變數的引用和value物件的引用,是不會釋放的,就會造成記憶體洩漏。
  3. 考慮這個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方法避免記憶體洩漏。

Java中的四種引用型別

上文中我們說到了WeakReference,大家可能對這個詞有點陌生,Java中有四種引用型別:

  1. 強引用:Java中預設的引用型別,一個物件如果具有強引用那麼只要這種引用還存在就不會被GC。
  2. 軟引用:簡言之,如果一個物件具有弱引用,在JVM發生OOM之前(即記憶體充足夠使用),是不會GC這個物件的;只有到JVM記憶體不足的時候才會GC掉這個物件。軟引用和一個引用佇列聯合使用,如果軟引用所引用的物件被回收之後,該引用就會加入到與之關聯的引用佇列中。
  3. 弱引用(這裡討論ThreadLocalMap中的Entry類的重點):如果一個物件只具有弱引用,那麼這個物件就會被垃圾回收器GC掉(被弱引用所引用的物件只能生存到下一次GC之前,當發生GC時候,無論當前記憶體是否足夠,弱引用所引用的物件都會被回收掉)。弱引用也是和一個引用佇列聯合使用,如果弱引用的物件被垃圾回收期回收掉,JVM會將這個引用加入到與之關聯的引用佇列中。若引用的物件可以通過弱引用的get方法得到,當引用的物件唄回收掉之後,再呼叫get方法就會返回null;
  4. 虛引用:虛引用是所有引用中最弱的一種引用,其存在就是為了將關聯虛引用的物件在被GC掉之後收到一個通知。(不能通過get方法獲得其指向的物件)。

我是御狐神,歡迎大家關注我的微信公眾號:wzm2zsd

本文最先發布至微信公眾號,版權所有,禁止轉載!