本文主要介绍 ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal 三者之间区别、如何使用、什么场景使用以及对原理和源码的介绍。介绍原理的时候通过最直白、最易懂的语言争取让大家了解三者之间的区别,以及日常如何把他们使用起来
ThreadLocal
ThreadLocal 解决的是每个线程可以拥有自己线程的变量实例。可以从隔离的角度解决变量线程安全的问题。
举个例子:
用户登陆后将用户的信息保存到 ThreadLocal 中,ThreadLocal<User> 可以保存请求过来的信息。也就是下面在这一个线程中任何地方都可以访问到这个 ThreadLocal 中的变量。虽然这是一个全局的静态变量,但是当有多个个线程调用 UserContext.setUser() 方法的时候,多个线程的变量都会保存,多个线程之间不会被相互覆盖。
看下下面代码,在同一个 JVM 进程中,虽然只有一个静态变量 userHolder,但是
线程A调用:UserContext.setUser(userA)
同时线程B调用:UserContext.setUser(userB)
在线程A中调用 UserContext.getUser() 得到的结果是 userA
在线程B中调用 UserContext.getUser() 得到的结果是 userB
public class UserContext {
private UserContext() {
}
private static ThreadLocal<User> userHolder = new ThreadLocal<>();
public static User getUser() {
return userHolder.get();
}
public static void setUser(User user) {
userHolder.set(user);
}
public static void clean() {
userHolder.remove();
}
}
实现原理:
ThreadLocal 是每个 Thread 都绑定一个 Map,线程之间不会互相干扰
这个Map 不是普通的 Map而是一个定制 Map:ThreadLocalMap。
这个Map 使用了使用“开放寻址法”中最简单的“线性探测法”解决散列冲突问题。这点是和我们平常使用的普通的 HashMap 不一样的。其中还是用了一个神奇的数字 HASH_INCREMENT= 0x61c88647,保证了一个完美的散列分布。具体可以参考斐波那契散列法的相关资料
我们看它的get源码:
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
从上面get源码的前两行可以看到,ThreadLocal的缺点:它不支持子线程。因为map是绑定在currentThread中的。子线程和父线程并不是一个Thread所以产生了ThreadLocal的进化版本
InheritableThreadLocal
前面说到ThreadLocal并不支持子线程,InheritableThreadLocal就是支持子线程的ThreadLocal
举个例子
public class UserContext {
private UserContext() {
}
private static InheritableThreadLocal<User> userHolder = new InheritableThreadLocal<>();
public static User getUser() {
return userHolder.get();
}
public static void setUser(User user) {
userHolder.set(user);
}
public static void clean() {
userHolder.remove();
}
}
@Test
public void test3() throws InterruptedException {
UserEntity userEntity = new UserEntity();
userEntity.setUsername("mzt");
UserContext.setUser(userEntity);
UserEntity user = UserContext.getUser();
Assert.assertNotNull(user);
Assert.assertEquals(user.getUsername(), "mzt");
new Thread(() -> {
final UserEntity user1 = UserContext.getUser();
Assert.assertNotNull(user1);
Assert.assertEquals(user1.getUsername(), "mzt");
}).start();
TimeUnit.MINUTES.sleep(1);
}
因为使用了InheritableThreadLocal这时候两个Assert都是正确的。但是仅仅使用ThreadLocal的时候上面例子中new Threa的run方法中getUser的返回值是为null的。如果把InheritableThreadLocal替换为ThreadLocal,那么new Thread中的
UserContext.getUser();返回值是NULL
原理
我们看 InheritableThreadLocal 的源码发现它并没有太多的方法,其实主要的代码是在 Thread 的 init() 方法中。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
可以看到 inheritableThreadLocals 还是一个 ThreadLocalMap,只不过是在 Thread 的 init 方法中把父Thread的inheritableThreadLocals 变量 copy 了一份给自己。同样借助 ThreadLocalMap 子线程可以获取到父线程的所有变量。
根据它的实现,我们也可以看到它的缺点,就是 Thread的 init 方法是在线程构造方法中 copy的,也就是在线程复用的线程池中是没有办法使用的。
TransmittableThreadLocal
上面介绍了 ThreadLocal 可以同线程共享变量。InheritableThreadLocal 可以父子线程共享变量,那么我们经常使用的线程池如何使用 ThreadLocal 这样的功能呢?TransmittableThreadLocal 简称(TTL)是阿里开源的一款支持线程池的 ThreadLocal 组件。
举个例子:
TenantContext 的实现是使用的 InheritableThreadLocal
public class TenantContext {
private static InheritableThreadLocal<String> tenantHolder = new InheritableThreadLocal<>();
// 其他方法略....
}
@Test
public void test1() throws InterruptedException {
ExecutorService ttlExecutorService = Executors.newFixedThreadPool(1);
TenantContext.setTenantId("mzt_" + 1);
ttlExecutorService.submit(() -> {
String tenantId = TenantContext.getTenantId();
log.info("#########, {}", tenantId);
//TenantContext.clean();
});
TimeUnit.SECONDS.sleep(2);
TenantContext.setTenantId("mzt_" + 2);
ttlExecutorService.submit(() -> {
log.info("#########, {}", TenantContext.getTenantId());
// TenantContext.clean();
});
Thread.sleep(2000L);
}
上面的例子中两次输出都是什么呢?
2021/01/24 21:49:24.678 pool-3-thread-1 [INFO] TenantContextTest (TenantContextTest.java:77) #########, mzt_1
2021/01/24 21:49:26.671 pool-3-thread-1 [INFO] TenantContextTest (TenantContextTest.java:83) #########, mzt_1
原因如下:例子中使用了固定线程数量为1的固定线程池。第一次sumbit task的时候,线程池会创建线程,因为使用了 InheritableThreadLocal ,所以调用 Thread 的 init 方法的时候会取父线程的 inheritableThreadLocals. 所以第一打印可以打印出 #########, mzt_1。下面又使用了 TenantContext.setTenantId("mzt_" + 2); 方法,之后又 submit 了一个线程,但是这时候线程池里面已经又线程了所以不会重新create线程了,也不会调用 thread 的 init 方法了,所以 get 出来的变量还是 mzt1 。
那么下面改成阿里的TTL
public class TenantContext {
private static TransmittableThreadLocal<String> tenantHolder = new TransmittableThreadLocal<>();
// 其他方法略....
}
@Test
public void test1() throws InterruptedException {
ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
TenantContext.setTenantId("mzt_" + 1);
ttlExecutorService.submit(() -> {
String tenantId = TenantContext.getTenantId();
log.info("#########, {}", tenantId);
//TenantContext.clean();
});
TimeUnit.SECONDS.sleep(2);
TenantContext.setTenantId("mzt_" + 2);
ttlExecutorService.submit(() -> {
log.info("#########, {}", TenantContext.getTenantId());
// TenantContext.clean();
});
Thread.sleep(2000L);
}
重新运行之后的结果:
2021/01/24 21:58:58.751 pool-3-thread-1 [INFO] TenantContextTest (TenantContextTest.java:77) #########, mzt_1
2021/01/24 21:59:00.746 pool-3-thread-1 [INFO] TenantContextTest (TenantContextTest.java:83) #########, mzt_2
原理
我们看到普通的线程池子被 TtlExecutors.getTtlExecutorService() 包裹了一下。如果仅仅是吧InheritableThreadLocal 修改为 TransmittableThreadLocal 是不起作用的。
所以 TTL 的做法也比较直接,使用了装饰器模式,既然 InheritableThreadLocal 只是在线程Create 的时候复制一份父线程数据,那么为了支持线程池就需要在 Thread 的 run 方法之前把父线程的数据 copy 一下就可以了。从源码中看是
public static ExecutorService getTtlExecutorService(ExecutorService executorService) {
if (executorService == null || executorService instanceof ExecutorServiceTtlWrapper) {
return executorService;
}
return new ExecutorServiceTtlWrapper(executorService);
}
重点:ExecutorServiceTtlWrapper包装了我们普通的 ExecutorService,然后充血了 submit 方法
@Override
public <T> Future<T> submit(Runnable task, T result) {
return executorService.submit(TtlRunnable.get(task), result);
}
重点就是 TtlRunnable 类了,它实现了 Runable 方法,并且重写了 run 方法
/**
* 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();
} finally {
TransmittableThreadLocal.restoreBackup(backup);
}
}
这个代码就是流程的核心了,就是在run方法之前复制了父线程的ThreadLocal变量。当线程执行时,调用 TtlRunnable run 方法,TtlRunnable 会从 AtomicReference 中获取出调用线程中所有的上下文,并把上下文给 TransmittableThreadLocal.Transmitter.replay 方法把上下文复制到当前线程。并把上下文备份。
当线程执行完,调用 TransmittableThreadLocal.Transmitter.restore 并把备份的上下文传入,恢复备份的上下文,把后面新增的上下文删除,并重新把上下文复制到当前线程。
延伸
我们发现通过对线程池包裹一层还是侵入性太强,不符合SOLID原则,不优雅。TTL也提供了通过agent方式接入的方法,具体可以在TTL官网看文档。这里就不介绍了。