ThreadLocal
概念
ThreadLocal,即線程變量,是一個以ThreadLocal對象爲鍵,任意對象爲值的存儲結構。這個結構被附帶在線程上,也就是說一個線程可以通過ThreadLocal對象查詢到綁定在這個線程上的一個值。
原理
關於ThreadLocal的原理,理清四個角色關係:Thread,ThreadLocal,ThreadLocalMap,Entry
在ThreadLocal中有個變量指向ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap是ThreadLocal的靜態內部類,當線程第一次執行set時,ThreadLocal會創建一個ThreadLocalMap對象,設置給Thread的threadLocals變量。
ThreadLocalMap中存放的是Entry,Entry是ThreadLocal和value的映射。
每一個線程都擁有一個ThreadLocalMap。
關於內存泄漏
ThreadLocal在ThreadLocalMap中是以一個弱引用身份被Entry中的Key引用的,因此如果ThreadLocal沒有外部強引用來引用它,那麼ThreadLocal會在下次JVM垃圾收集時被回收。
這個時候就會出現Entry中Key已經被回收,出現一個null Key的情況,外部讀取ThreadLocalMap中的元素是無法通過null Key來找到Value的。因此如果當前線程的生命週期很長,一直存在,那麼其內部的ThreadLocalMap對象也一直生存下來,這些null key就存在一條強引用鏈的關係:Thread --> ThreadLocalMap–>Entry–>Value,這條強引用鏈會導致Entry不會回收,Value也不會回收,但Entry中的Key卻已經被回收的情況,造成內存泄漏。
但是JVM團隊已經考慮到這樣的情況,並做了一些措施來保證ThreadLocal儘量不會內存泄漏:
- 在ThreadLocal的get()、set()、remove()方法調用的時候會清除掉線程ThreadLocalMap中所有Entry中Key爲null的Value,並將整個Entry設置爲null,利於下次內存回收Entry、value。
ThreadLocalMap處理Hash衝突
採用線性探測法來處理衝突,從當前位置往後找尋空位,空位指的是table[ i ] = null 或是 table[ i ] .key = null,將Entry插入該位置。也就是說一個Entry要麼在它的hash位置上,要麼就在該位置往後的某一位置上。
由於線性探測發 table 數組中的情況一定是一段一段連續的片段,我們將一個連續的片段稱爲 run。
關於線程安全性
每個線程都有自己的ThreadLocalMap,以及Entry[] 數組,只有自己操作,所以是線程安全的。那麼ThreadLocal呢?它並沒有可更改的狀態,所以也是線程安全的,來看看它的三個成員變量
// 每個ThreadLocal對象初始化後都會得到自己的hash值,之後不會再變
private final int threadLocalHashCode = nextHashCode();
// 靜態對象AtomicInteger,與ThreadLocal對象無關,
// 在第一次ThreadLocal類加載時初始化
private static AtomicInteger nextHashCode = new AtomicInteger();
// 不可變
private static final int HASH_INCREMENT = 0x61c88647;
所以說 ThreadLocal 也是線程安全的。
使用場景
常用於同一次請求的參數傳遞。比如說把身份信息埋到ThreadLocal中,然後該請求的所有接口都可以獲取到這個身份信息。
父子線程傳遞實現方案
如果子線程想要拿到父線程的中的ThreadLocal值怎麼辦呢?
錯誤的示例
比如會有以下的這種代碼的實現。由於ThreadLocal的實現機制,在子線程中get時,我們拿到的Thread對象是當前子線程對象,那麼他的ThreadLocalMap是null的,所以我們得到的value也是null。
private static void demo1() throws Exception {
Thread.currentThread().setName("主線程");
final ThreadLocal<String> threadLocal = new ThreadLocal<>();
// 調用set方法的時候,會初始化一個ThreadLocalMap
threadLocal.set("這個父線程設置的變量");
Thread subThread = new Thread(new Runnable() {
@Override
public void run() {
// 子線程獲取父線程的threadLocal,結果爲null
System.out.println("子線程獲取的變量爲 " +
threadLocal.get());
}
});
subThread.setName("子線程");
subThread.start();
}
public static void main(String[] args) throws Exception {
demo1();
}
那麼有沒有方法正確的獲取父線程中的ThreadLocal呢?
InheritableThreadLocal
那其實很多時候我們是有子線程獲得父線程ThreadLocal的需求的,要如何解決這個問題呢?這就是InheritableThreadLocal
這個類所做的事情。先來看下InheritableThreadLocal所做的事情。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* 重寫ThreadLocal類中的getMap方法,在原Threadlocal中是返回
* t.theadLocals,而在這麼卻是返回了inheritableThreadLocals,因爲
* Thread類中也有一個要保存父子傳遞的變量
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* 同理,在創建ThreadLocalMap的時候不是給t.threadlocal賦值
*而是給inheritableThreadLocals變量賦值
*
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
以上代碼大致的意思就是,如果你使用InheritableThreadLocal,那麼保存的所有東西都已經不在原來的t.thradLocals裏面,而是在一個新的t.inheritableThreadLocals變量中了。下面是Thread類中兩個變量的定義
/**
* 線程所持有的threadLocals
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
/**
* 線程所持有的inheritableThreadLocals,保持了從父線程繼承而來的本地變量信息
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
InheritableThreadLocal是如何實現在子線程中能拿到當前父線程中的值的呢?
一個常見的想法就是把父線程的所有的值都copy
到子線程中。
// Thread 線程類的初始化方法
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
//省略上面部分代碼
if (parent.inheritableThreadLocals != null)
//這句話的意思大致不就是,copy父線程parent的map,創建一個新的map賦值給當前線程的inheritableThreadLocals。
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//ignore
}
而且,在copy過程中是淺拷貝
,key和value都是原來的引用地址
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
// 獲取key
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// 獲取value
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
// 計算存放key的位置
int h = key.threadLocalHashCode & (len - 1);
// 線性探測法
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
到了這裏,大致的解釋了一下InheritableThreadLocal
爲什麼能解決父子線程傳遞Threadlcoal值的問題。
- 在創建新線程的時候會檢查父線程中t.inheritableThreadLocals變量是否爲null,如果不爲null則拷貝一份ThradLocalMap到子線程的t.inheritableThreadLocals成員變量中去
- 因爲複寫了getMap(Thread)和createMap()方法,所以調用get()方法的時候,就可以在getMap(t)的時候就會從t.inheritableThreadLocals中拿到map對象,從而實現了可以拿到父線程ThreadLocal中的值。
private static void demo2() throws Exception {
Thread.currentThread().setName("主線程");
final ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
// 調用set方法的時候,會初始化一個ThreadLocalMap
threadLocal.set("這個父線程設置的變量");
Thread subThread = new Thread(new Runnable() {
@Override
public void run() {
// 子線程獲取父線程的threadLocal
// 輸出爲: 子線程獲取的變量爲 這個父線程設置的變量
System.out.println("子線程獲取的變量爲 " +
threadLocal.get());
}
});
subThread.setName("子線程");
subThread.start();
}
InheritableThreadLocal不足
我們在使用線程的時候往往不會只是簡單的new Thread對象,而是使用線程池,當然線程池的好處多多。這裏不詳解,既然這裏提出了問題,那麼線程池會給InheritableThreadLocal帶來什麼問題呢?
我們列舉一下線程池的特點:
- 爲了減小創建線程的開銷,線程池會緩存已經使用過的線程
- 生命週期統一管理,合理的分配系統資源
如下示例:
private static void demo3() throws Exception {
final InheritableThreadLocal<String> inheritableThreadLocal =
new InheritableThreadLocal<>();
inheritableThreadLocal.set("xiexiexie");
//輸出 xiexiexie
System.out.println("父線程中獲取inheritableThreadLocal, 值爲:" +
inheritableThreadLocal.get());
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("子線程中獲取inheritableThreadLocal, 值爲:" +
inheritableThreadLocal.get());
inheritableThreadLocal.set("zhangzhangzhang");
System.out.println("子線程中獲取inheritableThreadLocal, 值爲:" +
inheritableThreadLocal.get());
}
};
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(runnable);
TimeUnit.SECONDS.sleep(1);
/**
* 第二次執行的時候,使用的是上一條線程,
* 並且InheritableThreadLocal只有在線程初始化的時候才從父線程繼承數據。
* 因此這次執行任務直接使用線程當前的InheritableThreadLocal
*/
executorService.submit(runnable);
TimeUnit.SECONDS.sleep(1);
System.out.println("父線程中獲取inheritableThreadLocal, 值爲:" +
inheritableThreadLocal.get());
executorService.shutdown();
}
可見,在使用線程池的情況,由於複用線程,所以造成InheriableThreadLocal被複用,從而導致無法使用父類的數據。
解決方案
如果我們能夠,在submit新任務的時候在重新從父線程中拷貝所有的變量。然後將這些變量賦值給當前線程的t.inhertableThreadLocal賦值。這樣就能夠解決在線程池中每一個新的任務都能夠獲得父線程中ThreadLocal中的值而不受其他任務的影響。Alibaba的一個庫解決了這個問題 [github:alibaba/transmittable-thread-local]
transmittable-thread-local實現原理
這個庫最簡單的方式是這樣使用的,通過簡單的修飾,使得提交的runable擁有了上一節所述的功能。具體的API文檔詳見github,這裏不再贅述。
TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
parent.set("value-set-in-parent");
Runnable task = new Task("1");
// 額外的處理,生成修飾了的對象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(task);
executorService.submit(ttlRunnable);
// Task中可以讀取, 值是"value-set-in-parent"
String value = parent.get();
這個方法TtlRunnable.get(task)最終會調用構造方法,返回的是該類本身,也是一個Runable,這樣就完成了簡單的裝飾。最重要的是在run方法這個地方。
public final class TtlRunnable implements Runnable {
private final AtomicReference<Map<TransmittableThreadLocal<?>, Object>> copiedRef;
private final Runnable runnable;
private final boolean releaseTtlValueReferenceAfterRun;
private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
//從父類copy值到本類當中
this.copiedRef = new AtomicReference<Map<TransmittableThreadLocal<?>, Object>>(TransmittableThreadLocal.copy());
this.runnable = runnable;//提交的runable,被修飾對象
this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}
/**
* wrap method {@link Runnable#run()}.
*/
@Override
public void run() {
Map<TransmittableThreadLocal<?>, Object> copied = copiedRef.get();
if (copied == null || releaseTtlValueReferenceAfterRun && !copiedRef.compareAndSet(copied, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}
//裝載到當前線程
Map<TransmittableThreadLocal<?>, Object> backup = TransmittableThreadLocal.backupAndSetToCopied(copied);
try {
runnable.run();//執行提交的task
} finally {
//clear
TransmittableThreadLocal.restoreBackup(backup);
}
}
}
在上面的使用線程池的例子當中,如果換成這種修飾的方式進行操作,B任務得到的肯定是父線程中ThreadLocal的值,解決了在線程池中InheritableThreadLocal不能解決的問題。
更新父線程ThreadLocal值?
如果線程之間出了要能夠得到父線程中的值,同時想更新值怎麼辦呢?在前面我們有提到,當子線程copy父線程的ThreadLocalMap的時候是淺拷貝的,代表子線程Entry裏面的value都是指向的同一個引用,我們只要修改這個引用的同時就能夠修改父線程當中的值了。
問題
ThreadLocal時要注意什麼?比如說內存泄漏?
需要主動調用remove()方法釋放無用的內存,原因查看上面的內存泄漏。