1. 程式人生 > 實用技巧 >ThreadLocal<T>原理解析

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。