ThreadLocal、ITL、TTL原理详解及实践
1.ThreadLocal 介绍
1.1基本使用
1.2原理分析
1.3软引用
2.InheritableThreadLocal 介绍
2.1基本使用
2.2原理分析
2.3ITL问题
3.TransmittableThreadLocal 介绍
3.1基本使用
3.2原理分析
3.3ITL问题
<h1 id='1'>一、ThreadLocal(TL)</h1>
项目中我们如果想要某个对象在程序运行中的任意位置获取到,就需要借助ThreadLocal来实现,这个对象称作线程的本地变量,下面就介绍下ThreadLocal是如何做到线程内本地变量传递的,
<h2 id='1.1'>1.1基本使用</h2>
//当前线程上下文
public static final ThreadLocal threadLocal = new ThreadLocal();
public static void main(String[] args) throws Exception {
//登录
LoginHandle();
System.out.println(String.format("当前线程名称: %s, 线程内数据为: %s",
Thread.currentThread().getName(), threadLocal.get()));
//查询
new Thread(() -> {
String info = getInfo();
System.out.println(String.format("当前线程名称: %s, 获取线程内数据为: %s",
Thread.currentThread().getName(), info));
//在子线程设置上下文,看是否影响主线程 上下文 值
threadLocal.set("ok2");
}).start();
//确保下面执行在上面的异步代码之后执行
Thread.sleep(1000);
System.out.println(String.format("当前线程名称: %s, 线程内数据为: %s",
Thread.currentThread().getName(), threadLocal.get()));
}
public static void LoginHandle() {
threadLocal.set("ok");
}
public static String getInfo() {
String o =(String) threadLocal.get();
threadLocal.remove();
return o;
}
/**打印
当前线程名称: main, 线程内数据为: ok
当前线程名称: Thread-0, 获取线程内数据为: null
当前线程名称: main, 线程内数据为: ok
*/
通过上面代码分析
- ThreadLocal 在多线程中是线程数据隔离的,线程之间不能访问彼此上下文的。
- ThreadLocal 数据共享只能在当前线程 操作数栈中。
<h2 id='1.2'>1.2原理分析</h2>
敬请期待
<h1 id='2'>二、InheritableThreadLocal</h1>
根据ThreadLocal(TL)特点,父线程的本地变量无法传递给子线程;InheritableThreadLocal(ITL)为了解决这个问题,可以实现 父线程的本地变量可以传递给子线程
<h2 id='2.1'>2.1基本使用</h2>
只修改下InheritableThreadLocal
//当前线程上下文
public static final InheritableThreadLocal threadLocal = new InheritableThreadLocal();
public static void main(String[] args) throws Exception {
//登录
LoginHandle();
System.out.println(String.format("当前线程名称: %s, 线程内数据为: %s",
Thread.currentThread().getName(), threadLocal.get()));
//查询
new Thread(() -> {
String info = getInfo();
System.out.println(String.format("当前线程名称: %s, 获取线程内数据为: %s",
Thread.currentThread().getName(), info));
//在子线程设置上下文,看是否影响主线程 上下文 值
threadLocal.set("ok2");
}).start();
//确保下面执行在上面的异步代码之后执行
Thread.sleep(1000);
System.out.println(String.format("当前线程名称: %s, 线程内数据为: %s",
Thread.currentThread().getName(), threadLocal.get()));
}
public static void LoginHandle() {
threadLocal.set("ok");
}
public static String getInfo() {
String o =(String) threadLocal.get();
threadLocal.remove();
return o;
}
/**打印
当前线程名称: main, 线程内数据为: ok
当前线程名称: Thread-0, 获取线程内数据为: ok
当前线程名称: main, 线程内数据为: ok
*/
<h2 id='2.2'>2.2原理分析</h2>
敬请期待
<h2 id='2.3'>2.3ITL问题</h2>
- 线程不安全
如果说线程本地变量是只读变量不会受到影响,但是如果是可写的,那么任意子线程针对本地变量的修改都会影响到主线程的本地变量(本质上是同一个对象),参考上面的第三个例子,子线程写入后会覆盖掉主线程的变量,也是通过这个结果,我们确认了子线程TLMap里变量指向的对象和父线程是同一个。 - 线程池中可能失效
按照上述实现,在使用线程池的时候,ITL会完全失效,因为父线程的TLMap是通过init一个Thread的时候进行赋值给子线程的,而线程池在执行异步任务时可能不再需要创建新的线程了,因此也就不会再传递父线程的TLMap给子线程了。
private static ExecutorService executorService = Executors.newFixedThreadPool(1);
public static final InheritableThreadLocal<String> itl = new InheritableThreadLocal();
public static void main(String[] args) {
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), itl.get()));
executorService.execute(() -> {
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), itl.get()));
});
itl.set("ok");//等上面的线程池第一次用完,父线程再进行赋值
executorService.execute(() -> {
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), itl.get()));
});
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), itl.get()));
}
/**
线程名称-main, 变量值=null
线程名称-pool-1-thread-1, 变量值=null
线程名称-pool-1-thread-1, 变量值=null
线程名称-main, 变量值=ok
*/
解决
但是,在实际项目中我们大多数采用线程池进行做异步任务,假如真的需要传递主线程的本地变量,使用ITL的问题显然是很大的,因为是有极大可能性拿不到任何值的,显然在实际项目中,ITL的位置实在是尴尬,所以在启用线程池的情况下,不建议使用ITL做值传递。为了解决这种问题,阿里做了transmittable-thread-local(TTL)来解决线程池异步值传递问题,下一篇,我们将会分析TTL的用法及原理。
<h1 id='3'>一、TransmittableThreadLocal(TTL)</h1>
首先,TTL是用来解决ITL解决不了的问题而诞生的,所以TTL一定是支持父线程的本地变量传递给子线程这种基本操作的,ITL也可以做到,但是前面有讲过,ITL在线程池的模式下,就没办法再正确传递了,所以TTL做出的改进就是即便是在线程池模式下,也可以很好的将父线程本地变量传递下去,
<h2 id='3.1'>2.2基本使用</h2>
private static ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(2));
//当前线程上下文
public static final TransmittableThreadLocal ttl = new TransmittableThreadLocal<>();
public static void main(String[] args) throws Exception {
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), ttl.get()));
executorService.execute(() -> {
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), ttl.get()));
});
ttl.set("ok");//等上面的线程池第一次用完,父线程再进行赋值
executorService.execute(() -> {
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), ttl.get()));
});
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), ttl.get()));
}
/**
线程名称-main, 变量值=null
线程名称-pool-1-thread-1, 变量值=null
线程名称-main, 变量值=ok
线程名称-pool-1-thread-2, 变量值=ok
*/
private static ThreadLocal tl = new TransmittableThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
String mainThreadName = "main_01";
tl.set(1);
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
sleep(1L); //确保上面的会在tl.set执行之前执行
tl.set(2); // 等上面的线程池第一次启用完了,父线程再给自己赋值
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
String mainThreadName = "main_02";
tl.set(3);
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
sleep(1L); //确保上面的会在tl.set执行之前执行
tl.set(4); // 等上面的线程池第一次启用完了,父线程再给自己赋值
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));
}).start();
}
private static void sleep(long time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
<h2 id='3.1'>3.2原理分析</h2>
敬请期待
<h2 id='3.2'>3.3总结</h2>
到这里基本上确认了TTL是如何进行线程池传值的,以及被包装的run方法执行异步任务之前,会使用replay进行设置父线程里的本地变量给当前子线程,任务执行完毕,会调用restore恢复该子线程原生的本地变量(目前原生本地变量的产生,就只碰到上述测试代码中的这一种情况,即线程第一次使用时通过ITL属性以及Thread的init方法传给子线程,还不太清楚有没有其他方式设置)。
其实,正常程序里想要完成线程池上下文传递,使用TL就足够了,我们可以效仿TTL包装线程池对象的原理,进行值传递,异步任务结束后,再remove,以此类推来完成线程池值传递,不过这种方式过于单纯,且要求上下文为只读对象,否则子线程存在写操作,就会发生上下文污染。
TTL项目地址(可以详细了解下它的其他特性和用法):https://github.com/alibaba/transmittable-thread-local
https://www.cnblogs.com/hama1993/p/10409740.html