ThreadLocal<T>原理解析
一、基本使用及含義
1.ThreadLocal<T>,直譯過來叫執行緒本地變數,執行緒隔離。
文件註釋:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
大概意思是ThreadLocal這個類的變數可以通過get和set方法讀寫一個執行緒自己獨立的變數副本;
ThreadLocal例項通常是類的私有靜態屬性,將狀態與之對應的執行緒相關聯。
換句話來說就是,一般我們使用ThreadLocal時,將其定義為private static,目的是把一些狀態的值和當前的執行緒進行隔離儲存,每個執行緒只能讀寫自己的狀態內容。
2.簡單使用
比如,web應用中統計每個請求的耗時,可以通過aop+ThreadLocal來實現(計算請求的消耗時間只是為了簡單使用ThreadLocal,實際開發中不會這樣做)。
@Aspect public class RequestMonitor {/* 計算Controller方法執行的總時間:Aop中計算,不用在Controller的每個方法中 消耗時間 = after()中的結束時間 - before()中的開始時間 因為是多執行緒併發訪問,所以不能直接定義成員變數 */ //執行緒隔離,用於記錄每個請求初始記憶體指標 - private static ThreadLocal<Long> startData = new ThreadLocal<>(); //切入點 所有的Controller中的所有方法 @Pointcut("execution(* xxx..controller.*Controller.*(..))")public void handle(){ } @Before("handle()") public void before(){ //記錄當前執行緒初始記憶體分配 單位:位元組 Long startTime = System.currentTimeMillis(); startData.set(startTime); } @After("handle()") public void after(){ //從ThreadLocal拿到當前執行緒的初始資料 Long startTime = startData.get(); Long endTime = System.currentTimeMillis(); Long cost = endTime - startTime; //把得到的資料做其他的處理,一般是MQ或者Redis,儘量不影響當前請求的效能 } }
每個請求(執行緒)get到的資料是自己set的資料!
3.應用場景
(1)Spring的宣告式事務,獲取資料庫連線時,一個事務裡(同一個執行緒),第一次從資料庫連線池中獲取,後面的都從ThreadLocal中獲取,這樣就能保證是同一個Connection;
(2)多執行緒讀寫共享變數時用於執行緒隔離操作;
(3)Session管理等。
二、原始碼解析
1.ThreadLocalMap
ThreadLocal的靜態內部類,作為Thread類的成員變數,由Thread負責管理其生命週期。
ThreadLocalMap內部維護了一個Entry陣列(可以resize進行擴容,初始容量為16,容量達到2/3時就開始擴容),類似於HashMap,也是一個key-value陣列,以ThreadLocal作為key,需要儲存的值作為value。至於上圖紅框中ThreadLocal為什麼由一個弱引用持有,後面會講。
Thread管理ThreadLocalMap如下:
(1)Thread類宣告ThreadLocalMap成員變數:
(2)Thread.init()中初始化:
(3)ThreadLocal中,建立執行緒t的ThreadLocalMap:
(4)當執行緒結束或者因異常打斷而退出時,會自動呼叫exit()方法,裡面會將ThreadLocalMap置為null,生命週期也就結束:
綜上,當一個執行緒建立時或ThreadLocal第一次set時,會初始化ThreadLocalMap,並有執行緒自己管理其生命週期。
當一個執行緒set的時候,如果ThreadLocalMap還沒有初始化,則ThreadLocal會為這個執行緒初始化一個ThreadLocalMap並將引用賦值到Thread.threadLocals,在exit()中回收,也就是執行緒退出的時候ThreadLocalMap就被回收。
2.ThreadLocal
set(T value):
首先獲取當前呼叫set()的執行緒,然後獲取這個執行緒的ThreadLocalMap(t.threadLocals);如果map為null,則建立一個ThreadLocalMap並將引用賦值給當前執行緒的threadLocals變數;如果不為null,則呼叫ThreadLocalMap.set(ThreadLocal<?> key,Object value)
private void set(ThreadLocal<?> key, Object value) { //獲取當前ThreadLocalMap的Entry陣列 Entry[] tab = table; //陣列的長度 int len = tab.length; //計算key的下標位置,hashcode和長度位運算,固定 int i = key.threadLocalHashCode & (len-1); //tab[i] != null,表示當前執行緒已經使用這個ThreadLocal變數已經呼叫過set for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { //獲取下標i位置的ThreadLocal物件並比較 ThreadLocal<?> k = e.get(); //如果相等,直接覆蓋之前的值(同一個執行緒多次呼叫set()時只保留最後一個) if (k == key) { e.value = value; return; } //如果k==null,大概表示ThreadLocal變數已經被回收,但程式中沒有呼叫remove(),導致value的值一直佔據著記憶體(記憶體洩漏) if (k == null) { replaceStaleEntry(key, value, i); return; } } //如果tab[i] == null,表示Entry陣列中還沒有存放當前ThreadLocal變數的值,那麼就會建立一個Entry物件 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
大概流程是:先判斷在Entry陣列指定下標位置(陣列長度不變的情況下,同一個ThreadLocal物件set時,會把值存入到Entry陣列固定下標的位置)是否為null,如果不為null,則直接在此下標處新建一個Entry(當前ThreadLocal變數,值);否則,比較傳入的ThreadLocal和這個位置上的ThreadLocal是不是同一個物件,如果是則直接替換value,如果可用為null,則將傳入的ThreadLocal變數和value直接替換。
總之,ThreadLocal.set()是把當前這個ThreadLocal變數和value封裝成一個Entry,存放到執行緒的ThreadLocalMap變數中,這樣就做到了執行緒隔離。
注:不同的ThreadLocal物件操作同一個執行緒的ThreadLocalMap,get/set操作會把ThreadLocal變數自身作為key傳入,放在Entry陣列不同下標位置,這樣就能區分不同的ThreadLocal物件放在同一個執行緒中的值。
get():
直接從當前執行緒的ThreadLocalMap中,Entry陣列固定的下標位置處獲取值;
如果在陣列該位置處的key和傳入的key指向同一個ThreadLocal(同一個引用),則直接返回Entry;否則,需要迴圈在陣列的其他位置查詢,如果找不到則返回null。
三、ThreadLocal中的兩種記憶體洩漏問題
1.在類中宣告的變數:ThreadLocal<T> tl = new ThreadLocal<>();此時tl是一個強引用指向堆中的ThreadLocal物件,同時ThreadLocalMap中的Entry物件是以同一個ThreadLocal作為key,
有些時候,當執行緒中的tl變數可能已經沒有指向堆中的ThreadLocal物件,但Entry中的key還指向這個ThreadLocal物件,如果Entry中的key也是一個強引用指向就導致堆中的ThreadLocal物件永遠都不能被垃圾回收,最終記憶體洩露。
所以JDK定義Entry的時候繼承自WeakReference<ThreadLocal<?>>,key持有的弱引用指向堆中的ThreadLocal,這樣當執行緒中的tl變數強引用不再指向堆中的ThreadLocal時,Entry中的key可以在下一次GC時被回收調,也就不會存在ThreadLocalMap中的key引起的記憶體洩露問題。
2.當ThreadLocal物件被回收了之後,由於某些執行緒執行的時間很長甚至一直在後臺執行,就不會執行exit()方法,ThreadLocalMap就不會被回收,由於key指向堆中的ThreadLocal物件已經變成了null,那麼value也就不能通過key被訪問,但實際上value物件還一直在堆中存在佔據著記憶體,所以還是會存在記憶體洩漏的問題。
這種記憶體洩漏的問題解決需要在程式中顯示地呼叫ThreadLocal.remove()方法,所以在當前執行緒不再需要指定的ThreadLocal儲存的資料時,就應該呼叫remove()。
e.clear():Reference中定義的,直接將當前的Entry引用置為null,GC可以回收。
expungeStaleEntity():清除掉無用的Entity,將key指向的value設定為null。