ThreadLocal(TL)、InheritableThreadLocal(ITL)和TransmittableThreadLocal(TTL)在不同場景下有不同用途,本文我們來分析一下 ...
1、概述
ThreadLocal(TL)是Java中一種線程局部變數實現機制,他為每個線程提供一個單獨的變數副本,保證多線程場景下,變數的線程安全。經常用於代替參數的顯式傳遞。
InheritableThreadLocal(ITL)是JDK提供的TL增強版,而TransmittableThreadLocal(TTL)是阿裡開源的ITL增強版
這些ThreadLocal在不同場景下有不同用途,我們來分析一下:
2、ThreadLocal
ThreadLocal主要的方法有四個:initialValue、set、get、remove
2.1、初始化——initialValule
當線程首次訪問該ThreadLocal時(ThreadLocal.get()),會進行初始化賦值。我們常用兩種方法初始化ThreadLocal
2.1.1、重寫initialValue
ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "";
}
};
2.1.2、調用ThreadLocal.withInitial
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "");
他會創建一個SuppliedThreadLocal內部類
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
該類重寫了initialValue方法
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
//當該線程首次訪問ThreadLocal時,會間接調用lambda表達式初始化
return supplier.get();
}
}
⚠️ITL並沒有重新實現withInitial,如果使用withInitial則會創建STL,失去自己增強的特性
2.2、賦值——set
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
這裡出現了一個關鍵屬性ThreadLocalMap,類定義在ThreadLocal中,是Thread的成員變數
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocalMap內部還有一個內部類Entry,是存值的地方
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
//ThreadLocal的引用是“key”
super(k);
//線程局部變數是value
value = v;
}
}
//Entry數組
//value具體放在哪個index下,是由ThreadLocal的hashCode算出來的
private Entry[] table;
}
2.3、取值——get
public T get() {
Thread t = Thread.currentThread();
//1、獲取線程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//2、根據ThreadLocal的hashCode,獲取對應Entry下的value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//3、如果沒有賦過值,則初始化
return setInitialValue();
}
2.4、清空——remove
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//會將對應Entry、包括他的key、value手動置null
m.remove(this);
}
3、InheritableThreadLocal
3.1、TL在父子線程場景下存在的問題
我們先來看一個例子
public static void main(String[] args) throws InterruptedException {
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "A");
threadLocal.set("B");
Thread thread = new Thread(() -> {
System.out.println("子線程ThreadLocal:" + threadLocal.get());
}, "子線程");
thread.start();
thread.join();
}
列印結果如下,可見子線程的ThreadLocal是初始值,並沒有使用父線程修改後的值:
子線程ThreadLocal:A
線程的ThreadLocalMap是首次訪問時創建的,所以子線程使用ThreadLocal的時候,會初始化一個新的ThreadLocal,線程局部變數為預設值
⚠️所以,TL不具有遺傳性
3.2、ITL的解決方案
為瞭解決TL子線程遺傳性的問題,JDK引入了ITL
他繼承ThreadLocal,重寫了childValue、getMap、createMap三個方法
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
這裡出現了inheritableThreadLocals,他存儲的就是從父線程拷貝過來的ThreadLocal,這個值是在父線程首次修改ThreadLocal的時候賦值的,然後在子線程創建時拷貝過來的
//父線程部分:
public void set(T value) {
Thread t = Thread.currentThread();
//該方法被ITL重寫,訪問inheritableThreadLocals為null
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//該方法同樣被ITL重寫,創建一個ThreadLocalMap賦值給inheritableThreadLocals
createMap(t, value);
}
//子線程部分:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//省略一些代碼...
//獲取當前線程(父線程、也就是創建子線程的線程)
Thread parent = currentThread();
//1、允許ThreadLocal遺傳(這個預設為true)
//2、inheritableThreadLocals不為空,因為父線程調用set了
//父線程不調用set,那ThreadLocal就是初始值,那直接初始化就好了,也不用進該分支
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
//createInheritedMap使用該構造函數,根據父線程的inheritableThreadLocals進行深拷貝
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
//深拷貝父線程ThreadLocalMap
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
//childValue被ITL重寫,返回父線程ThreadLocal的值
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
使用ITL的效果
public static void main(String[] args) throws InterruptedException {
ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>() {
@Override
protected String initialValue() {
return "A";
}
};
threadLocal.set("B");
Thread thread = new Thread(() -> {
System.out.println("子線程ThreadLocal:" + threadLocal.get());
}, "子線程");
thread.start();
thread.join();
}
列印結果如下,子線程拷貝了父線程ThreadLocal:
子線程ThreadLocal:B
總結一下,ITL解決父子線程遺傳性的核心思路是,將可遺傳的ThreadLocal放在父線程新的ThreadLocalMap中,在子線程首次使用時進行拷貝
4.、TransmittableThreadLocal
4.1、ITL線上程復用場景下存在的問題
我們再從一個簡單的例子說起
public static void main(String[] args) throws InterruptedException, ExecutionException {
ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>() {
@Override
protected String initialValue() {
return "A";
}
};
threadLocal.set("B");
ExecutorService executorService = Executors.newFixedThreadPool(1);
//1、子線程第一次獲取ThreadLocal
executorService.submit(() -> System.out.println("子線程ThreadLocal:"+threadLocal.get())).get();
Thread.sleep(1000);
//2、父線程修改ThreadLocal
threadLocal.set("C");
System.out.println("父線程修改ThreadLocal為"+threadLocal.get());
//3、子線程第二次獲取ThreadLocal
executorService.submit(() -> System.out.println("子線程ThreadLocal:"+threadLocal.get())).get();
}
列印結果如下,子線程在第二次列印時,並沒有拷貝父線程的ThreadLocal,使用的還是首次拷貝的值:
子線程ThreadLocal:B
父線程修改ThreadLocal為C
子線程ThreadLocal:B
⚠️可復用的子線程不會感知父線程ThreadLocal的變化
4.2、TTL的解決方案
4.2.1、TTL的使用
TTL在ITL上做了稍微複雜的封裝,我們從使用開始瞭解
引入依賴
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>latest</version>
</dependency>
在使用TTL時,線程需要經過TTL封裝,線程池同理
public static void main(String[] args) throws InterruptedException, ExecutionException {
ThreadLocal<String> threadLocal = new TransmittableThreadLocal<String>() {
@Override
protected String initialValue() {
return "A";
}
};
threadLocal.set("B");
ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
executorService.submit(() -> System.out.println("子線程ThreadLocal:" + threadLocal.get())).get();
Thread.sleep(1000);
threadLocal.set("C");
System.out.println("父線程修改ThreadLocal為" + threadLocal.get());
executorService.submit(() -> System.out.println("子線程ThreadLocal:" + threadLocal.get())).get();
Thread.sleep(1000);
executorService.submit(() -> {
threadLocal.set("D");
System.out.println("子線程修改ThreadLocal為" + threadLocal.get());
});
Thread.sleep(1000);
executorService.submit(() -> System.out.println("子線程ThreadLocal:" + threadLocal.get()));
Thread.sleep(1000);
}
列印結果如下,子線程每次都會獲取父線程的ThreadLocal
子線程ThreadLocal:B
父線程修改ThreadLocal為C
子線程ThreadLocal:C
子線程修改ThreadLocal為D
子線程ThreadLocal:C
從使用上看,TTL要求將任務封裝,那我們就從ThreadLocal和ExecutorService兩部分入手
4.2.2、TTL對ThreadLocal的封裝
下麵是TTL的取值和賦值邏輯,都涉及一個關鍵方法addThisToHolder,對應的屬性holder會線上程池執行任務時用到
//TransmittableThreadLocal.addThisToHolder()
private void addThisToHolder() {
//InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder
if (!holder.get().containsKey(this)) {
//holder是靜態變數,他會把TTL存到當前線程的map中
//value是null,他其實是把Map當Set用
//主線程賦值時,會獲取主線程的holderMap,然後把TTL存進去
holder.get().put((TransmittableThreadLocal<Object>) this, null);
}
}
@Override
public final void set(T value) {
if (!disableIgnoreNullValueSemantics && null == value) {
remove();
} else {
super.set(value);
//當主線程賦值時,會將自己的TTL放到自己的map中
addThisToHolder();
}
}
@Override
public final T get() {
T value = super.get();
if (disableIgnoreNullValueSemantics || null != value)
addThisToHolder();
return value;
}
4.2.3、TTL對任務的封裝
//我們通過TtlExecutors.getTtlExecutorService()對線程池進行封裝
public static ExecutorService getTtlExecutorService(@Nullable ExecutorService executorService) {
if (TtlAgent.isTtlAgentLoaded() || executorService == null || executorService instanceof TtlEnhanced) {
return executorService;
}
//入參是線程池,通過包裝類代理線程池的操作
return new ExecutorServiceTtlWrapper(executorService);
}
//ExecutorServiceTtlWrapper.submit()
public Future<?> submit(@NonNull Runnable task) {
//將提交的任務進行封裝
return executorService.submit(TtlRunnable.get(task));
}
4.2.3.1、任務構建
TtlRunnable構造方法
這裡都是主線程在操作,因為任務是主線程提交的
private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
this.capturedRef = new AtomicReference<Object>(capture());
this.runnable = runnable;
this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}
這裡有一個關鍵屬性capturedRef,他是一個原子引用,存了TTL
//TrasmitableThreadLocal.Transmitter
public static Object capture() {
//獲取ttl的值構建快照
return new Snapshot(captureTtlValues(), captureThreadLocalValues());
}
private static HashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() {
HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<TransmittableThreadLocal<Object>, Object>();
for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
//將主線程TTL的值存到當前任務中
ttl2Value.put(threadLocal, threadLocal.copyValue());
}
return ttl2Value;
}
4.2.3.2、任務執行
任務執行的代碼如下,在任務執行前回放ThreadLocal,在任務執行後恢復ThreadLocal:
這裡都是子線程在操作,因為任務都是子線程執行的
@Override
public void run() {
Object captured = capturedRef.get();
if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}
//1、備份子線程ThreadLocal
//2、使用主線程提交任務時構建的ThreadLocal副本,將子線程ThreadLocal覆蓋
Object backup = replay(captured);
try {
//3、任務執行
runnable.run();
} finally {
//3、使用之前備份的子線程ThreadLocal進行恢復
restore(backup);
}
}
總結一下,TTL讓子線程感知父線程變化的核心思路是,主線程在任務提交時構建ThreadLocal副本,在子線程執行任務時供其使用
⚠️提交和執行任務會對TTL進行若幹操作,理論上對性能有一點點影響,官方性能測試結論說損耗可忽略
作者:京東物流 劉朝永
來源:京東雲開發者 自猿其說