ThreadLocal(TL)是Java中一种线程局部变量实现机制,他为每个线程提供一个单独的变量副本,保证多线程场景下,变量的线程安全。经常用于代替参数的显式传递。
InheritableThreadLocal(ITL)是JDK提供的TL增强版,而TransmittableThreadLocal(TTL)是阿里开源的ITL增强版
这些ThreadLocal在不同场景下有不同用途,我们来分析一下:
ThreadLocal主要的方法有四个:initialValue、set、get、remove
当线程首次访问该ThreadLocal时(ThreadLocal.get()),会进行初始化赋值。我们常用两种方法初始化ThreadLocal
ThreadLocal threadLocal = new ThreadLocal() { @Override protected String initialValue() { return ""; }};
ThreadLocal threadLocal = ThreadLocal.withInitial(() -> "");
他会创建一个SuppliedThreadLocal内部类
public static ThreadLocal withInitial(Supplier extends S> supplier) { return new SuppliedThreadLocal<>(supplier);}
该类重写了initialValue方法
static final class SuppliedThreadLocal extends ThreadLocal { 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,失去自己增强的特性
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> { Object value; Entry(ThreadLocal> k, Object v) { //ThreadLocal的引用是“key” super(k); //线程局部变量是value value = v; } } //Entry数组 //value具体放在哪个index下,是由ThreadLocal的hashCode算出来的 private Entry[] table;}
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();}
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) //会将对应Entry、包括他的key、value手动置null m.remove(this); }
3.1、TL在父子线程场景下存在的问题
我们先来看一个例子
public static void main(String[] args) throws InterruptedException { ThreadLocal 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不具有遗传性
为了解决TL子线程遗传性的问题,JDK引入了ITL
他继承ThreadLocal,重写了childValue、getMap、createMap三个方法
public class InheritableThreadLocal extends ThreadLocal { 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
使用ITL的效果
public static void main(String[] args) throws InterruptedException { ThreadLocal threadLocal = new InheritableThreadLocal() { @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中,在子线程首次使用时进行拷贝
我们再从一个简单的例子说起
public static void main(String[] args) throws InterruptedException, ExecutionException { ThreadLocal threadLocal = new InheritableThreadLocal() { @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的变化
TTL在ITL上做了稍微复杂的封装,我们从使用开始了解
引入依赖
com.alibaba transmittable-thread-local latest
在使用TTL时,线程需要经过TTL封装,线程池同理
public static void main(String[] args) throws InterruptedException, ExecutionException { ThreadLocal threadLocal = new TransmittableThreadLocal() { @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两部分入手
下面是TTL的取值和赋值逻辑,都涉及一个关键方法addThisToHolder,对应的属性holder会在线程池执行任务时用到
//TransmittableThreadLocal.addThisToHolder()private void addThisToHolder() { //InheritableThreadLocal, ?>> holder if (!holder.get().containsKey(this)) { //holder是静态变量,他会把TTL存到当前线程的map中 //value是null,他其实是把Map当Set用 //主线程赋值时,会获取主线程的holderMap,然后把TTL存进去 holder.get().put((TransmittableThreadLocal) this, null); }}@Overridepublic final void set(T value) { if (!disableIgnoreNullValueSemantics && null == value) { remove(); } else { super.set(value); //当主线程赋值时,会将自己的TTL放到自己的map中 addThisToHolder(); }}@Overridepublic final T get() { T value = super.get(); if (disableIgnoreNullValueSemantics || null != value) addThisToHolder(); return value;}
//我们通过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));}
TtlRunnable构造方法
这里都是主线程在操作,因为任务是主线程提交的
private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) { this.capturedRef = new AtomicReference(capture()); this.runnable = runnable; this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;}
这里有一个关键属性capturedRef,他是一个原子引用,存了TTL
//TrasmitableThreadLocal.Transmitterpublic static Object capture() { //获取ttl的值构建快照 return new Snapshot(captureTtlValues(), captureThreadLocalValues());}private static HashMap, Object> captureTtlValues() { HashMap, Object> ttl2Value = new HashMap, Object>(); for (TransmittableThreadLocal threadLocal : holder.get().keySet()) { //将主线程TTL的值存到当前任务中 ttl2Value.put(threadLocal, threadLocal.copyValue()); } return ttl2Value;}
任务执行的代码如下,在任务执行前回放ThreadLocal,在任务执行后恢复ThreadLocal:
这里都是子线程在操作,因为任务都是子线程执行的
@Overridepublic 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进行若干操作,理论上对性能有一点点影响,官方性能测试结论说损耗可忽略