一文理解ThreadLocal
本文講解ThreadLocal、InheritableThreadLocal與TransmittableThreadLocal。
有關(guān)本文的實驗代碼,可以查看文末補充:“比較一下ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal在線程池復(fù)用線程的情況下的執(zhí)行情況”。
ThreadLocal
ThreadLocal的使用場景
分布式跟蹤系統(tǒng)
日志收集記錄系統(tǒng)上下文
Session級Cache
應(yīng)用容器或上層框架跨應(yīng)用代碼給下層SDK傳遞信息
舉例:
Spring的事務(wù)管理,用ThreadLocal存儲Connection,從而各個DAO可以獲取同一Connection,可以進(jìn)行事務(wù)回滾,提交等操作。
某些業(yè)務(wù)場景下,需要強(qiáng)制讀主庫來保證數(shù)據(jù)的一致性。在Sharding-JDBC中使用了ThreadLocal來存儲相關(guān)配置信息,實現(xiàn)優(yōu)雅的數(shù)據(jù)傳遞。
Spring Cloud Zuul用過濾器可以實現(xiàn)權(quán)限認(rèn)證,日志記錄,限流等功能,多個過濾器之間透傳數(shù)據(jù),底層使用了ThreadLocal。
在整個鏈路的日志中輸出當(dāng)前登錄的用戶ID,首先就得在攔截器獲取過濾器中獲取用戶。ID,然后將用戶ID進(jìn)行存儲到slf4j的MDC對象(底層使用ThreadLocal),然后進(jìn)行鏈路傳遞打印日志。
ThreadLocal的結(jié)構(gòu)
ThreadLocal的get()、set()方法,實際操作的都是Thread.currentThread(),即當(dāng)前線程的threadLocals變量。
threadLocals變量包含了一個map成員變量(ThreadLocalMap)。
ThreadLocalMap的key為當(dāng)前ThreadLocal, value為set的值。
相同的key在不同的散列表中的值必然是獨立的,每個線程都是在各自的散列表中執(zhí)行操作,如下圖所示:

ThreadLocal的set方法:
public void set(T value) {
//currentThread是個native方法,會返回對當(dāng)前執(zhí)行線程對象的引用。
Thread t = Thread.currentThread();
//getMap 返回線程自身的threadLocals
ThreadLocalMap map = getMap(t);
if (map != null) {
//把value set到線程自身的ThreadLocalMap中了
map.set(this, value);
} else {
//線程自身的ThreadLocalMap未初始化,則先初始化,再set
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal在set的時候,沒有進(jìn)行相應(yīng)的深拷貝,所以ThreadLocal要想做線程隔離,必須是基本類型或者是Runable實現(xiàn)類的局部變量。
ThreadLocal造成內(nèi)存泄漏
ThreadLocalMap內(nèi)部Entry:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
從代碼中看到,Entry繼承了WeakReference,并將ThreadLocal設(shè)置為了WeakReference,value設(shè)置為強(qiáng)引用。也就是:當(dāng)沒有強(qiáng)引用指向ThreadLocal變量時,它可被回收。
內(nèi)存泄漏風(fēng)險:ThreadLocalMap維護(hù)ThreadLocal變量與具體實例的映射,當(dāng)ThreadLocal變量被回收后(變?yōu)閚ull),無法路由到ThreadLocalMap。而該Entry還是在ThreadLocalMap中,從而這些無法清理的Entry,會造成內(nèi)存泄漏。
所以,在使用ThreadLocal的時候,會話結(jié)束前務(wù)必使用ThreadLocal.remove方法(remove方法會將Entry的value及Entry自身設(shè)置為null并進(jìn)行清理)。
ThreadLocal的最佳實踐
ThreadLocal使用時必須顯式地調(diào)用remove方法來避免內(nèi)存泄漏。
ThreadLocal對象建議使用static修飾。這樣做的好處是可以避免重復(fù)創(chuàng)建對象所導(dǎo)致的浪費(類第一次被使用時裝載,只分配一塊存儲空間)。壞處是正好形成內(nèi)存泄漏所需的條件(延長了ThreadLocal的生命周期,因此需要remove方法兜底)。
注釋說明使用場景。
對性能有極致要求可以參考開源框架優(yōu)化后的類,比如Netty的FastThreadLocal、Dubbo的InternalThreadLocal等。
InheritableThreadLocal
在全鏈路跟蹤框架中,Trace信息的傳遞功能是基于ThreadLocal的。但實際業(yè)務(wù)中可能會使用異步調(diào)用,這樣就會丟失Trace信息,破壞了鏈路的完整性。
此時可以使用JDK實現(xiàn)的InheritableThreadLocal,但它只支持父子線程間傳遞信息(例如:paramstream、new Thread等)。
Thread內(nèi)部為InheritableThreadLocal開辟了一個單獨的ThreadLocalMap(與ThreadLocal并列的成員變量)。在父線程創(chuàng)建一個子線程的時候,會檢查這個ThreadLocalMap是否為空,不為空則會淺拷貝給子線程的ThreadLocalMap。
從類的繼承層次來看,InheritableThreadLocal只是在ThreadLocal的get、set、remove流程中,重寫了getMap、createMap方法,整體流程與ThreadLocal保持一致。
Thread的init相關(guān)邏輯如下:
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
需要注意的是拷貝為淺拷貝。
TransmittableThreadLocal
InheritableThreadLocal可以在父線程創(chuàng)建子線程的時候?qū)hreadLocal中的值傳遞給子線程,從而完成鏈路跟蹤框架中的上下文傳遞。
但大部分業(yè)務(wù)應(yīng)用都會使用線程池,這種復(fù)用線程的池化場景中,線程池中的線程和主線程并不都是父子線程的關(guān)系,不能直接使用InheritableThreadLocal。
例如從Tomcat的線程(池化)提交task到業(yè)務(wù)線程池,就不能直接使用InheritableThreadLocal。
Transmittable ThreadLocal(簡稱TTL)是阿里開源的庫,繼承了InheritableThreadLocal,實現(xiàn)線程本地變量在線程池的執(zhí)行過程中,能正常的訪問父線程設(shè)置的線程變量。
TransmittableThreadLocal實現(xiàn)原理
InheritableThreadLocal不支持池化線程提交task到業(yè)務(wù)線程池的根本原因是,父線程創(chuàng)建子線程時,子線程InheritableThreadLocal只會復(fù)制一次環(huán)境變量。要支持線程池中能訪問提交任務(wù)線程的本地變量,只需要在線程向線程池提交任務(wù)時復(fù)制父線程的上下環(huán)境,那在線程池中就能夠訪問到父線程中的本地變量,實現(xiàn)本地環(huán)境變量在線程池調(diào)用中的透傳。
源碼見于參考文檔1,README有很詳細(xì)的講解,核心源碼也不難,建議看看。
此外,項目引入TTL的時候,可以使用Java Agent植入修飾代碼,修改runnable或者callable類,可以做到對應(yīng)用代碼無侵入(這個在README也有相關(guān)講解)。
補充說明
ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal在線程池復(fù)用線程的情況下的執(zhí)行情況如下:
1.線程局部變量為基礎(chǔ)類型
1.1 ThreadLocal
class TransmittableThreadLocalTest1 {
static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
static ExecutorService executorService =
Executors.newFixedThreadPool(1);
public static void main(String[] args) throws InterruptedException {
System.out.println("主線程開啟");
threadLocal.set(1);
System.out.println("主線程讀取本地變量:" + threadLocal.get());
executorService.submit(() -> {
System.out.println("子線程讀取本地變量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
threadLocal.set(2);
System.out.println("主線程讀取本地變量:" + threadLocal.get());
executorService.submit(() -> {
//[沒有讀到了主線程修改后的新值]
System.out.println("子線程讀取本地變量:" + threadLocal.get());
threadLocal.set(3);
System.out.println("子線程讀取本地變量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
//依舊讀取的是 2
System.out.println("主線程讀取本地變量:" + threadLocal.get());
}
}
輸出結(jié)果為:
主線程開啟
主線程讀取本地變量:1
子線程讀取本地變量:null
主線程讀取本地變量:2
子線程讀取本地變量:null
子線程讀取本地變量:3
主線程讀取本地變量:2
1.2 InheritableThreadLocal
class TransmittableThreadLocalTest2 {
static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
static ExecutorService executorService =
Executors.newFixedThreadPool(1);
public static void main(String[] args) throws InterruptedException {
System.out.println("主線程開啟");
threadLocal.set(1);
System.out.println("主線程讀取本地變量:" + threadLocal.get());
executorService.submit(() -> {
System.out.println("子線程讀取本地變量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
threadLocal.set(2);
System.out.println("主線程讀取本地變量:" + threadLocal.get());
executorService.submit(() -> {
//[沒有讀到了主線程修改后的新值]
System.out.println("子線程讀取本地變量:" + threadLocal.get());
threadLocal.set(3);
System.out.println("子線程讀取本地變量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
//依舊讀取的是 2
System.out.println("主線程讀取本地變量:" + threadLocal.get());
}
}
輸出結(jié)果為:
主線程開啟
主線程讀取本地變量:1
子線程讀取本地變量:1
主線程讀取本地變量:2
子線程讀取本地變量:1
子線程讀取本地變量:3
主線程讀取本地變量:2
1.3 TransmittableThreadLocal
class TransmittableThreadLocalTest3 {
static ThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();
static ExecutorService executorService =
TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
public static void main(String[] args) throws InterruptedException {
System.out.println("主線程開啟");
threadLocal.set(1);
System.out.println("主線程讀取本地變量:" + threadLocal.get());
executorService.submit(() -> {
System.out.println("子線程讀取本地變量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
threadLocal.set(2);
System.out.println("主線程讀取本地變量:" + threadLocal.get());
executorService.submit(() -> {
//[讀到了主線程修改后的新值]
System.out.println("子線程讀取本地變量:" + threadLocal.get());
threadLocal.set(3);
System.out.println("子線程讀取本地變量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
//依舊讀取的是 2
System.out.println("主線程讀取本地變量:" + threadLocal.get());
}
}
輸出結(jié)果為:
主線程開啟
主線程讀取本地變量:1
子線程讀取本地變量:1
主線程讀取本地變量:2
子線程讀取本地變量:2
子線程讀取本地變量:3
主線程讀取本地變量:2
2.線程局部變量為類對象
首先定義一個數(shù)據(jù)類:
@Data
@AllArgsConstructor
class UserSession{
String uuid;
String nickname;
}
2.1 ThreadLocal
class TransmittableThreadLocalTest4 {
static ThreadLocal<UserSession> threadLocal = new ThreadLocal<>();
static ExecutorService executorService =
Executors.newFixedThreadPool(1);
public static void main(String[] args) throws InterruptedException {
System.out.println("主線程開啟");
threadLocal.set(new UserSession("001","hello"));
System.out.println("主線程讀取本地變量:" + threadLocal.get());
executorService.submit(() -> {
System.out.println("子線程讀取本地變量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
threadLocal.get().setNickname("world");
System.out.println("主線程讀取本地變量:" + threadLocal.get());
executorService.submit(() -> {
//[沒有讀到了主線程修改后的新值]
System.out.println("子線程讀取本地變量:" + threadLocal.get());
threadLocal.get().setNickname("Java");
System.out.println("子線程讀取本地變量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
//依舊讀取的是 world
System.out.println("主線程讀取本地變量:" + threadLocal.get());
}
}
輸出結(jié)果為:
主線程開啟
主線程讀取本地變量:UserSession(uuid=001, nickname=hello)
子線程讀取本地變量:null
主線程讀取本地變量:UserSession(uuid=001, nickname=world)
子線程讀取本地變量:null
主線程讀取本地變量:UserSession(uuid=001, nickname=world)
2.2 InheritableThreadLocal
class TransmittableThreadLocalTest5 {
static ThreadLocal<UserSession> threadLocal = new InheritableThreadLocal<>();
static ExecutorService executorService =
Executors.newFixedThreadPool(1);
public static void main(String[] args) throws InterruptedException {
System.out.println("主線程開啟");
threadLocal.set(new UserSession("001","hello"));
System.out.println("主線程讀取本地變量:" + threadLocal.get());
executorService.submit(() -> {
System.out.println("子線程讀取本地變量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
threadLocal.get().setNickname("world");
System.out.println("主線程讀取本地變量:" + threadLocal.get());
executorService.submit(() -> {
//[讀到了主線程修改后的新值]
System.out.println("子線程讀取本地變量:" + threadLocal.get());
threadLocal.get().setNickname("Java");
System.out.println("子線程讀取本地變量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
//讀取的是 Java(因為淺拷貝)
System.out.println("主線程讀取本地變量:" + threadLocal.get());
}
}
主線程開啟
主線程讀取本地變量:UserSession(uuid=001, nickname=hello)
子線程讀取本地變量:UserSession(uuid=001, nickname=hello)
主線程讀取本地變量:UserSession(uuid=001, nickname=world)
子線程讀取本地變量:UserSession(uuid=001, nickname=world)
子線程讀取本地變量:UserSession(uuid=001, nickname=Java)
主線程讀取本地變量:UserSession(uuid=001, nickname=Java)
2.3 InheritableThreadLocal
class TransmittableThreadLocalTest6 {
static ThreadLocal<UserSession> threadLocal = new TransmittableThreadLocal<>();
static ExecutorService executorService =
TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
public static void main(String[] args) throws InterruptedException {
System.out.println("主線程開啟");
threadLocal.set(new UserSession("001","hello"));
System.out.println("主線程讀取本地變量:" + threadLocal.get());
executorService.submit(() -> {
System.out.println("子線程讀取本地變量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
threadLocal.get().setNickname("world");
System.out.println("主線程讀取本地變量:" + threadLocal.get());
executorService.submit(() -> {
//[讀到了主線程修改后的新值]
System.out.println("子線程讀取本地變量:" + threadLocal.get());
threadLocal.get().setNickname("Java");
System.out.println("子線程讀取本地變量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
//讀取的是 Java(因為淺拷貝)
System.out.println("主線程讀取本地變量:" + threadLocal.get());
}
}
輸出結(jié)果與上面2.2的結(jié)果一樣
參考文檔:
https://github.com/alibaba/transmittable-thread-local
