一、前言
這篇部落格來分析一下ThreadLocal
的實現原理以及常見問題,由於現在時間比較晚了,我就不廢話了,直接進入正題。
二、正文
2.1 ThreadLocal是什麼
在講實現原理之前,我先來簡單的說一說ThreadLocal
是什麼。ThreadLocal
被稱作執行緒區域性變數,當我們定義了一個ThreadLocal
變數,所有的執行緒共同使用這個變數,但是對於每一個執行緒來說,實際操作的值是互相獨立的。簡單來說就是,ThreadLocal能讓執行緒擁有自己內部獨享的變數。舉一個簡單的例子:
// 定義一個執行緒共享的ThreadLocal變數
static ThreadLocal<Integer> tl = new ThreadLocal<>();
public static void main(String[] args) {
// 建立第一個執行緒
Thread t1 = new Thread(() -> {
// 設定ThreadLocal變數的初始值,為1
tl.set(1);
// 迴圈列印ThreadLocal變數的值
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "----" + tl.get());
// 每次列印完讓值 + 1
tl.set(tl.get() + 1);
}
}, "thread1");
// 建立第二個執行緒
Thread t2 = new Thread(() -> {
// 設定ThreadLocal變數的初始值,為100,與上一個執行緒區別開
tl.set(100);
// 迴圈列印ThreadLocal變數的值
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "----" + tl.get());
// 每次列印完讓值 - 1
tl.set(tl.get() - 1);
}
}, "thread2");
// 開啟兩個執行緒
t1.start();
t2.start();
tl.remove();
}
上面的程式碼,執行結果如下(注:每次執行的結果可能不同):
thread1----1
thread2----100
thread1----2
thread2----99
thread1----3
thread2----98
thread1----4
thread2----97
thread1----5
thread2----96
thread1----6
thread2----95
thread1----7
thread2----94
thread1----8
thread2----93
thread1----9
thread2----92
thread1----10
thread2----91
通過上面的輸出結果我們可以發現,執行緒1
和執行緒2
雖然使用的是同一個ThreadLocal
變數儲存值,但是輸出結果中,兩個執行緒的值卻互不影響,執行緒1
從1
輸出到10
,而執行緒2
從100
輸出到91
。這就是ThreadLocal
的功能,即讓每一個執行緒擁有自己獨立的變數,多個執行緒之間互不影響。
2.2 ThreadLocal的實現原理
下面我就就來說一說ThreadLocal
是如何做到執行緒之間相互獨立的,也就是它的實現原理。這裡我直接放出結論,後面再根據原始碼分析:每一個執行緒都有一個對應的Thread物件,而Thread類有一個成員變數,它是一個Map集合,這個Map集合的key就是ThreadLocal的引用,而value就是當前執行緒在key所對應的ThreadLocal中儲存的值。當某個執行緒需要獲取儲存在ThreadLocal變數中的值時,ThreadLocal底層會獲取當前執行緒的Thread物件中的Map集合,然後以ThreadLocal作為key,從Map集合中查詢value值。這就是ThreadLocal
實現執行緒獨立的原理。也就是說,ThreadLocal
能夠做到執行緒獨立,是因為值並不存在ThreadLocal
中,而是儲存線上程物件中。下面我們根據ThreadLocal
中兩個最重要的方法來確認這一點。
2.3 ThreadLocal中的get方法
get
方法的作用非常簡單,就是執行緒向ThreadLocal
中取值,下面我們來看看它的原始碼:
public T get() {
// 獲取當前執行緒的Thread物件
Thread t = Thread.currentThread();
// getMap方法傳入Thread物件,此方法將返回Thread物件中儲存的一個Map集合
// 這個Map集合的型別為ThreadLocalMap,這是ThreadLoacl的一個內部類
// 當前執行緒存放在ThreadLocal中的值,實際上存放在這個Map集合中
ThreadLocalMap map = getMap(t);
// 如果當前Map集合已經初始化,則直接從Map集合中查詢
if (map != null) {
// ThreadLocalMap的key其實就是ThreadLoacl物件的引用
// 所以要找到執行緒在當前ThreadLoacl中存放的值,就需要以當前ThreadLoacl作為key
// getEntry方法就是通過key獲取map中的一個key-value,而這裡使用的key就是this
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果返回值不為空,表示查詢成功
if (e != null) {
@SuppressWarnings("unchecked")
// 於是獲取對應的value並返回
T result = (T)e.value;
return result;
}
}
// 若當前執行緒的ThreadLocalMap還未初始化,或者查詢失敗,則呼叫以下方法
return setInitialValue();
}
private T setInitialValue() {
// 此方法預設返回null,但是可以由子類進行重新,根據需求返回需要的值
T value = initialValue();
// 獲取當前執行緒的Thread物件
Thread t = Thread.currentThread();
// 獲取對應的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果Map已經初始化了,就直接往map中加入一個key-value
// key就是當前ThreadLocal物件的引用,而value就是上面獲取到的value,預設為null
if (map != null)
map.set(this, value);
// 若還沒有初始化,則呼叫createMap建立ThreadLocalMap物件
else
createMap(t, value);
// 返回initialValue方法返回的值,預設為null
return value;
}
void createMap(Thread t, T firstValue) {
// 建立ThreadLocalMap物件,構造方法傳入的是第一對放入其中的key-value
// 這個key也就是當前執行緒第一次呼叫get方法的ThreadLocal物件,也就是當前ThreadLocal物件
// 而firstValue則是initialValue方法的返回值,預設為null
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
上面的程式碼非常直觀的驗證了我之前說過的ThreadLocal
的實現原理。通過上面的程式碼,我們可以非常直觀的看到,執行緒向ThreadLocal
中存放的值,最後都放入了執行緒自己的ThreadLocalMap
中,而這個map
的key
就是當前ThreadLocal
的引用。而ThreadLocal
中,獲取執行緒的ThreadLocalMap
的方法getMap
的程式碼如下:
ThreadLocalMap getMap(Thread t) {
// 直接返回Thread物件的threadLocals成員變數
return t.threadLocals;
}
我們再看看Thread
類中的threadLocals
變數:
/** 可以看到,ThreadLocalMap是ThreadLocal的內部類 */
ThreadLocal.ThreadLocalMap threadLocals = null;
2.4 ThreadLocal中的set方法
下面再來看一看ThreadLocal
的set
方法的實現,set
方法用來使執行緒向ThreadLocal
中存放值(實際上是存放線上程自己的Map
中):
public void set(T value) {
// 獲取當前執行緒的Thread物件
Thread t = Thread.currentThread();
// 獲取當前執行緒的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 若map已經初始化,則之際將value放入Map中,對應的key就是當前ThreadLocal的引用
if (map != null)
map.set(this, value);
// 若沒有初始化,則呼叫createMap方法,為當前執行緒t建立ThreadLocalMap,
// 然後將key-value放入(此方法已經在上面講解get方法是看過)
else
createMap(t, value);
}
這就是set
方法的實現,比較簡單。看完上面兩個關鍵方法的實現,相信大家對ThreadLocal
的實現已經有了一個比較清晰的認識,下面我們來更加深入的分析ThreadLocal
,看看ThreadLocalMap
的一些實現細節。
2.5 ThreadLocalMap的中的弱引用
ThreadLocalMap
的實現其實就是一個比較普通的Map
集合,它的實現和HashMap
類似,所以具體的實現細節我們就不一一講解了,這裡我們只關注它最特別的一個地方,即它內部的節點Entry
。我們先來看看Entry
的程式碼:
// Entry是ThreadLocalMap的內部類,表示Map的節點
// 這裡繼承了WeakReference,這是java實現的弱引用類,泛型為ThreadLocal
// 表示在這個Map中,作為key的ThreadLocal是弱引用
// (這裡value是強引用,因為沒用WeakReference)
static class Entry extends WeakReference<ThreadLocal<?>> {
/** 儲存value */
Object value;
Entry(ThreadLocal<?> k, Object v) {
// 將key的值傳入父類WeakReference的構造方法,用弱引用來引用key
super(k);
// value則直接使用上面的強引用
value = v;
}
}
可以看到,上面的Entry
比較特殊,它繼承自WeakReference
型別,這是Java
實現的弱引用。在具體講解前,我們先來介紹一下不同型別的引用:
強引用:這是Java中最常見的引用,在沒有使用特殊引用的情況下,都是強引用,比如Object o = new Object()就是典型的強引用。能讓程式設計師通過強引用訪問到的物件,不會被JVM垃圾回收,即使記憶體空間不夠,JVM也不會回收這些物件,而是丟擲記憶體溢位異常;
軟引用:軟引用描述的是一些還有用,但不是必須的物件。被軟引用所引用的物件,也不會被垃圾回收,直到JVM將要發生記憶體溢位異常時,才會將這些物件列為回收物件,進行回收。在JDK1.2之後,提供了SoftReference類實現軟引用;
弱引用:弱引用描述的是非必須的物件,被弱引用所引用的物件,只能生存到下一次垃圾回收前,下一次垃圾回收來臨,此物件就會被回收。在JDK1.2之後,提供了WeakReference類實現弱引用(也就是上面Entry繼承的類);
虛引用:這是最弱的一種引用關係,一個物件是否有虛引用,完全不會對其生存時間產生影響,我們也不能通過一個虛引用訪問物件,使用虛引用的唯一目的就是,能在這個物件被回收時,受到一個系統的通知。JDK1.2之後,提供了PhantomReference實現虛引用;
介紹完各類引用的概念,我們就可以來分析一下Entry
為什麼需要繼承WeakReference
類了。從程式碼中,我們可以看到,Entry
將key
值,也就是ThreadLocal
的引用傳入到了WeakReference
的構造方法中,也就是說在ThreadLocalMap
中,key
的引用是弱引用。這表明,當沒有其他強引用指向key
時,這個key
將會在下一次垃圾回收時被JVM
回收。
為什麼需要這麼做呢?這麼做的目的自然是為了有利於垃圾回收了。如果瞭解過JVM
的垃圾回收演算法的應該知道,JVM
判斷一個物件是否需要被回收,判斷的依據是這個物件還能否被我們所使用,舉個簡單的例子:
public static void main(String[] args) {
Object o = new Object();
o = null;
}
上面的程式碼中,我們建立了一個物件,並使用強引用o
指向它,然後我們將o
置為空,這個時候剛剛建立的物件就丟失了,因為我們無法通過任何引用找到這個物件,從而使用它,於是這個物件就需要被回收,這種判斷依據被稱為可達性分析。關於JVM
的垃圾回收演算法,可以參考這篇部落格:Java中的垃圾回收演算法詳解。
好,迴歸正題,我們開始分析為什麼ThreadLocalMap
需要讓key
使用弱引用。假設我們建立了一個ThreadLocal
,使用完之後沒有用了,我們希望能夠讓它被JVM
回收,於是有了下面這個過程:
// 建立ThreadLocal物件
ThreadLocal tl = new ThreadLocal();
// .....省略使用的過程...
// 使用完成,希望被JVM回收,於是執行以下操作,解除強引用
tl = null;
我們在使用完ThreadLocal
之後,解除對它的強引用,希望它被JVM
回收。但是JVM
無法回收它,因為我們雖然在此處釋放了對它的強引用,但是它還有其它強引用,那就是Thread
物件的ThreadLocalMap
的key
。我們之前反覆說過,ThreadLocalMap
的key
就是ThreadLocal
物件的引用,若這個引用是一個強引用,那麼在當前執行緒執行完畢,被回收前,ThreadLocalMap
不會被回收,而ThreadLocalMap
不會被回收,它的key
引用的ThreadLocal
也就不會回收,這就是問題的所在。而使用弱引用就可以保證,在其他對ThreadLocal的強引用解除後,ThreadLocalMap對它的引用不會影響JVM對它進行垃圾回收。這就是使用弱引用的原因。
2.6 ThreadLocal造成的記憶體溢位問題
上面描述了對ThreadLocalMap
對key
使用弱引用,來避免JVM
無法回收ThreadLocal
的問題,但是這裡卻還有另外一個問題。我們看上面Entry
的程式碼發現,key
值雖然使用的弱引用,但是value使用的卻是強引用。這會造成一個什麼問題?這會造成key被JVM回收,但是value卻無法被收,key對應的ThreadLocal被回收後,key變為了null,但是value卻還是原來的value,因為被ThreadLocalMap所引用,將無法被JVM回收。若value
所佔記憶體較大,執行緒較多的情況下,將持續佔用大量記憶體,甚至造成記憶體溢位。我們通過一段程式碼演示這個問題:
public class Main {
public static void main(String[] args) {
// 迴圈建立多個TestClass
for (int i = 0; i < 100; i++) {
// 建立TestClass物件
TestClass t = new TestClass(i);
// 呼叫反覆
t.printId();
// *************注意此處,非常關鍵:為了幫助回收,將t置為null
t = null;
}
}
static class TestClass {
int id;
// 每個TestClass物件對應一個很大的陣列
int[] arr = new int[100000000];
// 每個TestClass物件對應一個ThreadLocal物件
ThreadLocal<int[]> threadLocal = new ThreadLocal<>();
TestClass(int id) {
this.id = id;
// threadLocal存放的就是這個很大的陣列
threadLocal.set(arr);
}
public void printId() {
System.out.println(id);
}
}
}
上面的程式碼多次建立所佔記憶體非常大的物件,並在建立後,立即解除物件的強引用,讓物件可以被JVM
回收。按道理來說,上面的程式碼執行應該不會發生記憶體溢位,因為我們雖然建立了多個大物件,佔用了大量空間,但是這些物件立即就用不到了,可以被垃圾回收,而這個物件被垃圾回收後,物件的id
,陣列,和threadLocal
成員都會被回收,所以所佔記憶體不會持續升高,但是實際執行結果如下:
0
1
2
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at Main$TestClass.<init>(Main.java:23)
at Main.main(Main.java:10)
可以看到,很快就發生了記憶體溢位異常。為什麼呢?需要注意到,在TestClass
的構造方法中,我們將陣列arr
放入了ThreadLocal
物件中,也就是被放進了當前執行緒的ThreadLocalMap
中,作為value
存在。我們前面說過,ThreadLocalMap
的value
是強引用,這也就意味著雖然ThreadLocal
可以被正常回收,但是作為value
的大陣列無法被回收,因為它仍然被ThreadLocalMap
的強引用所指向。於是TestClass
物件的超大陣列就一種在記憶體中,佔據大量空間,我們連續建立了多個TestClass
,記憶體很快就被佔滿了,於是發生了記憶體溢位。而JDK
的開發人員自然發現了這個問題,於是有了下面這個解決方案:
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
TestClass t = new TestClass(i);
t.printId();
// **********注意,與上面的程式碼只有此處不同************
// 此處呼叫了ThreadLocal物件的remove方法
t.threadLocal.remove();
t = null;
}
}
static class TestClass {
int id;
int[] arr;
ThreadLocal<int[]> threadLocal;
TestClass(int id) {
this.id = id;
arr = new int[100000000];
threadLocal = new ThreadLocal<>();
threadLocal.set(arr);
}
public void printId() {
System.out.println(id);
}
}
}
上面的程式碼中,我們在將t
置為空時,先呼叫了ThreadLocal
物件的remove
方法,這樣做了之後,再看看執行結果:
0
1
2
// ....神略中間部分
98
99
做了上面的修改後,沒有再發生記憶體溢位異常,程式正常執行完畢。這是為什麼呢?ThreadLocal
的remove
方法究竟有什麼作用。其實remove
方法的作用非常簡單,執行remove
方法時,會從當前執行緒的ThreadLocalMap
中刪除key
為當前ThreadLocal
的那一個記錄,key
和value
都會被置為null,這樣一來,就解除了ThreadLocalMap
對value
的強引用,使得value
可以正常地被JVM
回收了。所以,今後如果我們確認不再使用的ThreadLocal
物件,一定要記得呼叫它的remove
方法。
我們之前說過,如果我們沒有呼叫remove
方法,那就會導致ThreadLocal
在使用完畢後,被正常回收,但是ThreadLocalMap
中存放的value
無法被回收,此時將會在ThreadLocalMap
中出現key
為null
,而value
不為null
的元素。為了減少已經無用的物件依舊佔用記憶體的現象,ThreadLocal
底層實現中,在操作ThreadLocalMap
的過程中,執行緒若檢測到key
為null
的元素,會將此元素的value
置為null
,然後將這個元素從ThreadLocalMap
中刪除,佔用的記憶體就可以讓JVM
將其回收。比如說在getEntry
方法中,或者是Map
擴容的方法中等。
三、總結
ThreadLocal
實現執行緒獨立的方式是直接將值存放在Thread
物件的ThreadLocalMap
中,Map
的key
就是ThreadLocal
的引用,且為了有助於JVM
進行垃圾回收,key
使用的是弱引用。在使用ThreadLocal
後,一定要記得呼叫remove
方法,有助於JVM
對value
的回收。
四、參考
- 《深入理解Java虛擬機器(第二版)》
- https://mp.weixin.qq.com/s/Y24LQwukYwXueTS6NG2kKA