前言
多线程中,除了加锁可以解决共享变量的不一致情况,还有就是使用threadlocal进行隔离。前者有锁等待的消耗,后者利用空间换取时间,每个线程中持有一份变量副本进行隔离。常见的日志中MDC是采用threadloacl实现的。
一.threadlocal原理
threadlocal引用关系如下图:
其中,使用完threadlocal需要调用remove方法进行清空,否则会有内存泄漏风险。设计者也做了一些防范措施,在ThreadLocal
的get
,set
,remove
方法,都会清除线程ThreadLocalMap
里所有key
为null
的value
。
二.线程间传递
1.new Thread直接创建线程
不复用的情况下,InheritableThreadLocal可以将父线程的内容传递到子线程
2.使用线程池
线程池会复用线程,InheritableThreadLocal只有在首次创建线程的时候会将父线程的内容传递到子线程,复用的时候,保存的是刚创建子线程时父线程的内容,此时无法获取到最新父线程的内容。
1.直接包装Runnable
void runOnCurrentThread() {
// 从当前线程上获取MDC的所有数据
Map tags = MDC.getCopyOfContextMap();
Runable task = () -> {
// 记录当前线程原有的数据,任务执行结束后再回复这些参数
Map local = MDC.getCopyOfContextMap();
// 在新线程上将上一个线程的MDC设置进去
MDC.setContextMap(tags);
try {
runOnNextThread();
} finally {
// 恢复当前线程上的MDC
MDC.setContextMap(local);
}
}
// 在当前线程上,把task提交到线程池,task则会在threadpool中的某个线程上执行
threadpool.execute(task);
}
void runOnNextThread() {
// 跑在第二个线程上
// do sth
}
2.实现线程池的execute
ExecuterService threadpool = new new ThreadPoolExecutor(...省略) {
@Override
public void execute(Runnable command) {
// 从当前线程上获取MDC的所有数据
Map tags = MDC.getCopyOfContextMap();
Runable wrap = () -> {
// 记录当前线程原有的数据,任务执行结束后再回复这些参数
Map local = MDC.getCopyOfContextMap();
// 在新线程上将上一个线程的MDC设置进去
MDC.setContextMap(tags);
try {
// 执行真正的任务
command.run();
} finally {
// 恢复当前线程上的MDC
MDC.setContextMap(local);
}
}
super.execute(wrap);
}
}
// 下面这部分代码与第一版一模一样
void runOnCurrentThread() {
Runable task = () -> runOnNextThread();
// 在当前线程上,把task提交到线程池,task则会在threadpool中的某个线程上执行
threadpool.execute(task);
}
void runOnNextThread() {
// 跑在第二个线程上
// do sth
}
3.使用ali的transmittable-thread-local
ExecutorService executorService = Executors.newFixedThreadPool(1);
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");
Runnable task = () -> {
// 在子线程中读取
String value = context.get();
println("task get context value: " + value);
};
// 额外的处理,生成修饰了的对象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(task);
executorService.submit(ttlRunnable);
context.set("value-set-in-parent-2");
ttlRunnable = TtlRunnable.get(task);
executorService.submit(ttlRunnable);
// =====================================================
Thread.sleep(1000);
executorService.shutdown();
注意:在子线程中,需要先保存当前线程的threadlocal存储的值,操作完成后,在恢复原来的值。父子线程是两个线程,是没有问题的,如果是以下情况,必须要恢复原始值
1.上面提到的场景,线程池满了 且 线程池使用的是『CallerRunsPolicy』,
则 提交到线程池的任务 在capture线程直接执行,也就是 直接在业务线程中同步执行;
2.使用ForkJoinPool(包含并行执行Stream与CompletableFuture,底层使用ForkJoinPool)的场景,展开的ForkJoinTask会在调用线程中直接执行。
总结:
1.threadlocal的原理以及防止内存泄漏
2.父子线程之间的threadlocal存储变量的传递