Java多執行緒程式設計 透徹理解ThreadLocal的原理
ThreadLocal可以說是筆試面試的常客,每逢面試基本都會問到,關於ThreadLocal的原理以及不正當的使用造成的OOM記憶體溢位的問題,值得花時間仔細研究一下其原理。這一篇主要學習一下ThreadLocal的原理,在下一篇會深入理解一下OOM記憶體溢位的原理和最佳實踐。
ThreadLocal很容易讓人望文生義,想當然地認為是一個“本地執行緒”。其實,ThreadLocal並不是一個Thread,而是Thread的一個區域性變數,也許把它命名為ThreadLocalVariable更容易讓人理解一些。
當使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本。
ThreadLocal 的作用是提供執行緒內的區域性變數,這種變數線上程的生命週期內起作用,減少同一個執行緒內多個函式或者元件之間一些公共變數的傳遞的複雜度。
從執行緒的角度看,目標變數就像是執行緒的本地變數,這也是類名中“Local”所要表達的意思。
一、ThreadLocal全部方法和內部類
ThreadLocal全部方法和內部類結構如下:
ThreadLocal公有的方法就四個,分別為:get、set、remove、intiValue:
也就是說我們平時使用的時候關心的是這四個方法。
ThreadLocal是如何做到為每一個執行緒維護變數的副本的呢?
其實實現的思路很簡單:在ThreadLocal類中有一個static宣告的Map,用於儲存每一個執行緒的變數副本,Map中元素的鍵為執行緒物件,而值對應執行緒的變數副本。我們自己就可以提供一個簡單的實現版本:
執行結果:
雖然上面的程式碼清單中的這個ThreadLocal實現版本顯得比較簡單粗,但其目的主要在與呈現JDK中所提供的ThreadLocal類在實現上的思路。
二、ThreadLocal原始碼分析
1、執行緒區域性變數在Thread中的位置
既然是執行緒區域性變數,那麼理所當然就應該儲存在自己的執行緒物件中,我們可以從 Thread 的原始碼中找到執行緒區域性變數儲存
我們可以看到執行緒區域性變數是儲存在Thread物件的 threadLocals 屬性中,而 threadLocals 屬性是一個 ThreadLocal.ThreadLocalMap 物件。
ThreadLocalMap為ThreadLocal的靜態內部類,如下圖所示:
2、Thread和ThreadLocalMap的關係
Thread和ThreadLocalMap的關係,先看下邊這個簡單的圖,可以看出Thread中的threadLocals就是ThreadLocal中的ThreadLocalMap:
到這裡應該大致能夠感受到上述三者之間微妙的關係,再看一個複雜點的圖:
可以看出每個thread例項都有一個ThreadLocalMap。在上圖中的一個Thread的這個ThreadLocalMap中分別存放了3個Entry,預設一個ThreadLocalMap初始化了16個Entry,每一個Entry物件存放的是一個ThreadLocal變數物件。
再看一張網路上的圖片,應該可以更好的理解,如下圖:
這裡的Map其實是ThreadLocalMap。
3、ThreadLocalMap與WeakReference
ThreadLocalMap從字面上就可以看出這是一個儲存ThreadLocal物件的map(其實是以它為Key),不過是經過了兩層包裝的ThreadLocal物件:
(1)第一層包裝是使用 WeakReference<ThreadLocal<?>> 將ThreadLocal物件變成一個弱引用的物件;
(2)第二層包裝是定義了一個專門的類 Entry 來擴充套件 WeakReference<ThreadLocal<?>>:
類 Entry 很顯然是一個儲存map鍵值對的實體,ThreadLocal<?>為key, 要儲存的執行緒區域性變數的值為value。super(k)呼叫的WeakReference的建構函式,表示將ThreadLocal<?>物件轉換成弱引用物件,用做key。
4、ThreadLocalMap 的建構函式
可以看出,ThreadLocalMap這個map的實現是使用一個數組 private Entry[] table 來儲存鍵值對的實體,初始大小為16,ThreadLocalMap自己實現瞭如何從 key 到 value 的對映:
使用一個 static 的原子屬性 AtomicInteger nextHashCode,通過每次增加 HASH_INCREMENT = 0x61c88647 ,然後 & (INITIAL_CAPACITY - 1) 取得在陣列 private Entry[] table 中的索引。
總的來說,ThreadLocalMap是一個類似HashMap的集合,只不過自己實現了定址,也沒有HashMap中的put方法,而是set方法等區別。
三、ThreadLocal的set方法
由於每個thread例項都有一個ThreadLocalMap,所以在進行set的時候,首先根據Thread.currentThread()獲取當前執行緒,然後根據當前執行緒t,呼叫getMap(t)獲取ThreadLocalMap物件, 如果是第一次設定值,ThreadLocalMap物件是空值,所以會進行初始化操作,即呼叫createMap(t,value)方法:
即是呼叫上述的構造方法進行構造,這裡僅僅是初始化了16個元素的引用陣列,並沒有初始化16個 Entry 物件。而是一個執行緒中有多少個執行緒區域性物件要儲存,那麼就初始化多少個 Entry 物件來儲存它們。
到了這裡,我們可以思考一下,為什麼要這樣實現了。
1、為什麼要用 ThreadLocalMap 來儲存執行緒區域性物件呢?
原因是一個執行緒擁有的的區域性物件可能有很多,這樣實現的話,那麼不管你一個執行緒擁有多少個區域性變數,都是使用同一個 ThreadLocalMap 來儲存的,ThreadLocalMap 中 private Entry[] table 的初始大小是16。超過容量的2/3時,會擴容。
然後在回到如果map不為空的情況,會呼叫map.set(this, value);方法,我們看到是以當前 thread 的引用為 key, 獲得 ThreadLocalMap ,然後呼叫 map.set(this, value); 儲存進 private Entry[] table :
可以看到,set(T value)方法為每個Thread物件都建立了一個ThreadLocalMap,並且將value放入ThreadLocalMap中,ThreadLocalMap作為Thread物件的成員變數儲存。那麼可以用下圖來表示ThreadLocal在儲存value時的關係。
2、瞭解了set方法的大致原理之後,我們在研究一段程式如下:
這樣的話就相當於一個執行緒依附了三個ThreadLocal物件,執行完最後一個set方法之後,除錯過程如下:
可以看到table(Entry集合)中有三個物件,物件的值就是我們設定的三個threadLocal的物件值;
3、如果在修改一下程式碼,修改為兩個執行緒:
這樣的話,可以看到執行除錯圖如下:
然後更改到Thread2,檢視,由於多執行緒,執行緒1執行到上圖情況,執行緒2執行到下圖情況,也可以看出他們是不同的ThreadLocalMap:
那如果多個執行緒,只設置一個ThreadLocal變數那,結果可想而知,這裡不再贅述!
另外,有一點需要提示一下,程式碼如下:
執行結果:
可以看到,在這個執行緒中的ThreadLocal變數的值始終是隻有一個的,即以前的值被覆蓋了的!這裡是因為Entry物件是以該ThreadLocal變數的引用為key的,所以多次賦值以前的值會被覆蓋,特此注意!
到這裡應該可以清楚了的瞭解Thread、ThreadLocal和ThreadLocalMap之間的關係了!
四、ThreadLocal的get方法
經過上述set方法的分析,對於get方法應該理解起來輕鬆了許多,首先獲取ThreadLocalMap物件,由於ThreadLocalMap使用的當前的ThreadLocal作為key,所以傳入的引數為this,然後呼叫getEntry()方法,通過這個key構造索引,根據索引去table(Entry陣列)中去查詢執行緒本地變數,根據下邊找到Entry物件,然後判斷Entry物件e不為空並且e的引用與傳入的key一樣則直接返回,如果找不到則呼叫getEntryAfterMiss()方法。呼叫getEntryAfterMiss表示直接雜湊到的位置沒找到,那麼順著hash表遞增(迴圈)地往下找,從i開始,一直往下找,直到出現空的槽為止。
五、ThreadLocal的記憶體回收
ThreadLocal 涉及到的兩個層面的記憶體自動回收:
(1)在 ThreadLocal 層面的記憶體回收:
當執行緒死亡時,那麼所有的儲存在的執行緒區域性變數就會被回收,其實這裡是指執行緒Thread物件中的 ThreadLocal.ThreadLocalMap threadLocals會被回收,這是顯然的。
(2)ThreadLocalMap 層面的記憶體回收:
如果執行緒可以活很長的時間,並且該執行緒儲存的執行緒區域性變數有很多(也就是 Entry 物件很多),那麼就涉及到線上程的生命期內如何回收 ThreadLocalMap 的記憶體了,不然的話,Entry物件越多,那麼ThreadLocalMap 就會越來越大,佔用的記憶體就會越來越多,所以對於已經不需要了的執行緒區域性變數,就應該清理掉其對應的Entry物件。
使用的方式是,Entry物件的key是WeakReference 的包裝,當ThreadLocalMap 的 private Entry[] table,已經被佔用達到了三分之二時 threshold = 2/3(也就是執行緒擁有的區域性變數超過了10個) ,就會嘗試回收 Entry 物件,我們可以看到 ThreadLocalMap.set()方法中有下面的程式碼:
cleanSomeSlots 就是進行回收記憶體:
六、ThreadLocal可能引起的OOM記憶體溢位問題簡要分析
我們知道ThreadLocal變數是維護在Thread內部的,這樣的話只要我們的執行緒不退出,物件的引用就會一直存在。當執行緒退出時,Thread類會進行一些清理工作,其中就包含ThreadLocalMap,Thread呼叫exit方法如下:
但是,當我們使用執行緒池的時候,就意味著當前執行緒未必會退出(比如固定大小的執行緒池,執行緒總是存在的)。如果這樣的話,將一些很大的物件設定到ThreadLocal中(這個很大的物件實際儲存在Thread的threadLocals屬性中),這樣的話就可能會出現記憶體溢位的情況。
一種場景就是說如果使用了執行緒池並且設定了固定的執行緒,處理一次業務的時候存放到ThreadLocalMap中一個大物件,處理另一個業務的時候,又一個執行緒存放到ThreadLocalMap中一個大物件,但是這個執行緒由於是執行緒池建立的他會一直存在,不會被銷燬,這樣的話,以前執行業務的時候存放到ThreadLocalMap中的物件可能不會被再次使用,但是由於執行緒不會被關閉,因此無法釋放Thread 中的ThreadLocalMap物件,造成記憶體溢位。
也就是說,ThreadLocal在沒有執行緒池使用的情況下,正常情況下不會存在記憶體洩露,但是如果使用了執行緒池的話,就依賴於執行緒池的實現,如果執行緒池不銷燬執行緒的話,那麼就會存在記憶體洩露。所以我們在使用執行緒池的時候,使用ThreadLocal要格外小心!
public class Main {
private static final int THREAD_LOOP_SIZE = 2;
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
test2();
}
private static void test2() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_LOOP_SIZE);
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "set(abc)" );
threadLocal.set("abc");
});
Thread.sleep(300);
for (int i = 0; i < THREAD_LOOP_SIZE + 8; i++) {
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "~~" + threadLocal.get());
});
}
}
}
執行結果:
pool-1-thread-1set(abc)
pool-1-thread-1~~abc
pool-1-thread-2~~null
pool-1-thread-1~~abc
pool-1-thread-2~~null
pool-1-thread-1~~abc
pool-1-thread-2~~null
pool-1-thread-1~~abc
pool-1-thread-2~~null
pool-1-thread-1~~abc
pool-1-thread-2~~null
很明顯,執行緒池開啟2條執行緒,執行緒1釋放後如果再次被利用,其儲存的ThreadLocalMap並未釋放
解決方案: 用完執行緒後呼叫threadLocal.remove() ,清空該執行緒的區域性變數, 防止記憶體洩漏和溢位
private static void test2() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_LOOP_SIZE);
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "set(abc)" );
threadLocal.set("abc");
threadLocal.remove();
});
Thread.sleep(300);
for (int i = 0; i < THREAD_LOOP_SIZE + 8; i++) {
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "~~" + threadLocal.get());
});
}
}
執行結果:
pool-1-thread-1set(abc)
pool-1-thread-1~~null
pool-1-thread-2~~null
pool-1-thread-1~~null
pool-1-thread-2~~null
pool-1-thread-1~~null
pool-1-thread-1~~null
pool-1-thread-2~~null
pool-1-thread-1~~null
pool-1-thread-2~~null
pool-1-thread-1~~null
七、總結
通過原始碼可以看到每個執行緒都可以獨立修改屬於自己的副本而不會互相影響,從而隔離了執行緒和執行緒.避免了執行緒訪問例項變數發生安全問題. 同時我們也能得出下面的結論:
(1)ThreadLocal只是操作Thread中的ThreadLocalMap物件的集合;
(2)ThreadLocalMap變數屬於執行緒的內部屬性,不同的執行緒擁有完全不同的ThreadLocalMap變數;
(3)執行緒中的ThreadLocalMap變數的值是在ThreadLocal物件進行set或者get操作時建立的;
(4)使用當前執行緒的ThreadLocalMap的關鍵在於使用當前的ThreadLocal的例項作為key來儲存value值;
(5) ThreadLocal模式至少從兩個方面完成了資料訪問隔離,即縱向隔離(執行緒與執行緒之間的ThreadLocalMap不同)和橫向隔離(不同的ThreadLocal例項之間的互相隔離);
(6)一個執行緒中的所有的區域性變數其實儲存在該執行緒自己的同一個map屬性中;
(7)執行緒死亡時,執行緒區域性變數會自動回收記憶體;
(8)執行緒區域性變數時通過一個 Entry 儲存在map中,該Entry 的key是一個 WeakReference包裝的ThreadLocal, value為執行緒區域性變數,key 到 value 的對映是通過:ThreadLocal.threadLocalHashCode & (INITIAL_CAPACITY - 1) 來完成的;
(9)當執行緒擁有的區域性變數超過了容量的2/3(沒有擴大容量時是10個),會涉及到ThreadLocalMap中Entry的回收;
對於多執行緒資源共享的問題,同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。前者僅提供一份變數,讓不同的執行緒排隊訪問,而後者為每一個執行緒都提供了一份變數,因此可以同時訪問而互不影響。
八、專案實戰應用
1.登入的時候將隨機token和使用者資訊存到redis
stringRedisTemplate.opsForValue().set(RedisConstants.REDIS_KEY_ADMIN + token, JsonUtils.serialize(admin), RedisConstants.REDIS_LOGIN_TIME_OUT, TimeUnit.HOURS);
2.攔截器(訪問每個介面都會經過),從redis取出使用者物件,儲存到當前執行緒
Object redisObj = stringRedisTemplate.opsForValue().get(RedisConstants.REDIS_KEY_ADMIN + adminToken);
Admin admin = JsonUtils.deserializeJsonToObject(jsonRedisObj, new Admin());
MyThreadLocal.getAdminThreadLocal().set(admin);
3.取出當前執行緒中的登入資訊
Admin adminThreadLocal = MyThreadLocal.getAdminThreadLocal().get();
參考文章:
1、https://www.toutiao.com/i6584564064377111053
2、http://blog.csdn.net/shenlei19911210/article/details/50060223
3、http://www.cnblogs.com/digdeep/p/4510875.html