一、前言
這篇博客來分析一下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
