本文主要介绍ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal三者之间区别、如何使用、什么场景使用以及对原理和源码的介绍。介绍原理的时候通过最直白、最易懂的语言争取让大家了解三者之间的区别,以及日常如何把他们使用起来
ThreadLocal解决的是每个线程可以拥有自己线程的变量实例。可以从隔离的角度解决变量线程安全的问题。
用户登陆后将用户的信息保存到ThreadLocal中,ThreadLocal 可以保存请求过来的信息。也就是下面在这一个线程中任何地方都可以访问到这个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 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的进化版本
前面说到ThreadLocal并不支持子线程,InheritableThreadLocal就是支持子线程的ThreadLocal
public class UserContext {
private UserContext() {
}
private static InheritableThreadLocal 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的,也就是在线程复用的线程池中是没有办法使用的。
上面介绍了ThreadLocal可以同线程共享变量。InheritableThreadLocal可以父子线程共享变量,那么我们经常使用的线程池如何使用ThreadLocal这样的功能呢?TransmittableThreadLocal简称(TTL)是阿里开源的一款支持线程池的ThreadLocal组件。
TenantContext的实现是使用的InheritableThreadLocal
public class TenantContext {
private static InheritableThreadLocal 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 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 Future 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, Object> copied = copiedRef.get();
if (copied == null || releaseTtlValueReferenceAfterRun && !copiedRef.compareAndSet(copied, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}
Map, 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官网看文档。这里就不介绍了。
https://www.cnblogs.com/dennyzhangdd/p/7978455.html
https://www.javaspecialists.eu/archive/Issue164-Why-0x61c88647.html
https://blog.xieyangzhe.com/archives/45