在web开发中使用了ThreadLocal本地线程存储拦截器解析的用户信息,方便在下文代码中调用,但是在springboot中使用@Async开启异步操作时,就会造成,子线程无法拿到父本地线程数据。拿到一些脏数据。
1.InheritableThreadLocal
- 在这个问题上,java为我们提供了InheritableThreadLocal类,该类是继承了ThreadLocal,它具有与普通ThreadLocal相同的功能,但还具有一个额外的特性,即可以在子线程中继承父线程的变量值。
- 普通的ThreadLocal变量只是在每个线程内部创建副本,而InheritableThreadLocal可以将父线程的变量值传递给子线程。这对于需要跨线程传递上下文信息或者共享线程间状态的场景非常有用。
private static ThreadLocal<String> fatherInheritableThread = new InheritableThreadLocal<>();
探讨
使用InheritableThreadLocal类可以实现父子线程的共享,通过源码发现,我们当前线程A里正在创建子线程A-1,A线程去判断自己的inheritableThreadLocals是否有值,有值返回给当前正在创建的A-1的inheritableThreadLocals属性。
注意,这里代码有点绕,一看咋是自己赋值给自己,但仔细来看, Thread parent = currentThread();
parent是当前正在new Thread()的线程(正在创造A-1),是A-1线程拿到了当前正在运行自己的A线程的inheritableThreadLocals然后this.inheritableThreadLocals = inheritableThreadLocals赋值给了自己。this当然就是A-1了。A线程是父线程其数据当然是我们主动set的了。到此,一个新的子线程A-1诞生了。
子线程A-1创建完后,InheritableThreadLocal.get()获取数据,get方法没有被重写,其位于父类ThreadLocal里面。
InheritableThreadLocal重写getMap,使之在get方法时,获取this.InheritableThreadLocals的数据,
在构造ThreadLocalMap时,调用了InheritableThreadLocal重写的childValue,直接返回父线程值。
容易混淆的是,在这里,我们操作的是inheritableThreadLocals属性,可以理解为对于一个线程,threadLocals与inheritableThreadLocals是平级关系。
问题
也就是说,只有当子线程被new的时候,父子线程才会同步数据,对于使用线程池模式线程复用的情况下,就会出现多线程问题,即A线程的value=5,A线程任务结束后,执行B任务,A线程至始至终没有被销毁,所以B任务始终无法获取父线程最新的数据,从而导致污染。InheritableThreadLocal并没有改变Thread类的行为,而是在Thread类中新增了一个成员变量来存储线程的变量副本。它利用了Java的继承机制,在子线程创建时复制父线程的变量副本,实现了父线程变量传递给子线程的功能。
当创建一个新的子线程时,Java会调用Thread类的init() 方法来初始化该线程。在init()方法中,会通过inheritableThreadLocals的createInheritedMap(ThreadLocalMap parentMap)方法来创建一个新的InheritableThreadLocalMap对象,并将父线程的inheritableThreadLocals中的值复制到新的InheritableThreadLocalMap对象中。如果线程被复用,则不会调用init方法从而没有以上效应。
线程池线程复用实例:
package com.liubingzhe.smartFile;
import org.springframework.core.task.TaskDecorator;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
public class ThreadTest {
private static ThreadLocal<String> fatherThread = new ThreadLocal<>();
private static ThreadLocal<String> fatherInheritableThread = new InheritableThreadLocal<>();
static class FirstTask implements Runnable {
@Override
public void run() {
System.out.println("[FirstTask] print ThreadLocal: " + fatherThread.get());
System.out.println("[FirstTask] print InheritableThreadLocal:" + fatherInheritableThread.get());
}
}
static class SecondTask implements Runnable {
@Override
public void run() {
System.out.println("[SecondTask] print ThreadLocal: " + fatherThread.get());
System.out.println("[SecondTask] print InheritableThreadLocal:" + fatherInheritableThread.get());
}
}
public static void main(String[] args) throws InterruptedException {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
// 限制线程数为1
threadPoolTaskExecutor.setMaxPoolSize(1);
threadPoolTaskExecutor.setCorePoolSize(1);
threadPoolTaskExecutor.setQueueCapacity(1);
threadPoolTaskExecutor.setThreadNamePrefix("async-");
threadPoolTaskExecutor.initialize();
String printText = "this is father thread print";
fatherInheritableThread.set(printText);
fatherThread.set(printText);
System.out.println("父线程初始数据");
System.out.println("fatherThread print: " + fatherThread.get());
System.out.println("fatherInheritableThread print: " + fatherInheritableThread.get());
System.out.println("子线程1开始执行");
threadPoolTaskExecutor.execute(new FirstTask());
printText = "change text";
fatherInheritableThread.set(printText);
Thread.sleep(2000);
System.out.println("子线程2开始执行");
threadPoolTaskExecutor.execute(new SecondTask());
}
}
我们限制线程池仅允许一个线程使之复用,并执行这两个异步,中途我们修改printText的值,观察结果:
父线程初始数据
fatherThread print: this is father thread print
fatherInheritableThread print: this is father thread print
子线程1开始执行
[FirstTask] print ThreadLocal: null
[FirstTask] print InheritableThreadLocal:this is father thread print
子线程2开始执行
[SecondTask] print ThreadLocal: null
[SecondTask] print InheritableThreadLocal:this is father thread print
可以发现子线程2并没有发生改变。通过在子线程中增加 System.out.println(Thread.currentThread().getId());
可以进一步确定是一个线程在运行,当然也只能是一个。
怎么解决?
- TransmittableThreadLocal可以解决
- 使用线程池装饰器
threadPoolTaskExecutor.setTaskDecorator(new myDecorator());
- 在线程运行前,获取当前父线程上下文的值
- 执行前给当前任务创建一个新的本地线程,并将父线程上下文数据赋值给当前新的本地线程,然后赋值给父线程(本质创建了一个父线程副本)
static class myDecorator implements TaskDecorator{
@Override
public Runnable decorate(Runnable runnable) {
String fatherValue = fatherInheritableThread.get();
return ()->{
InheritableThreadLocal<String> objectThreadLocal = new InheritableThreadLocal<>();
try{
objectThreadLocal.set(fatherValue);
fatherThread = objectThreadLocal;
runnable.run();
}finally {
objectThreadLocal.remove();
}
};
}
}
在一个线程情况下的打印结果,数据发生改变
父线程初始数据
fatherThread print: this is father thread print
fatherInheritableThread print: this is father thread print
子线程1开始执行
12
[FirstTask] print ThreadLocal: this is father thread print
[FirstTask] print InheritableThreadLocal:this is father thread print
子线程2开始执行
12
[SecondTask] print ThreadLocal: change text
[SecondTask] print InheritableThreadLocal:this is father thread print
第二种方法是我在开发过程中临时想到了一个解决方法,可能存在一些问题,但是后来通过来了解TransmittableThreadLocal类发现,基本思路一致,都是创建了线程副本,但是其是通过在切换线程时,还原了线程状态。
TransmittableThreadLocal是一个第三方库,它通过字节码增强和拦截线程切换的方式实现了在线程之间传递上下文的功能。它是基于InheritableThreadLocal的变体,并且专门用于解决在线程池等场景下的线程传递问题。
下面是TransmittableThreadLocal的基本原理:
字节码增强:TransmittableThreadLocal使用字节码增强技术,在运行时修改Java字节码。在线程类(Thread)和线程池类(ThreadPoolExecutor)的相关方法中,插入了额外的代码。
线程切换的拦截:TransmittableThreadLocal使用ThreadLocal类的特性,通过继承ThreadLocal并重写相关方法来实现对线程切换的拦截。具体是重写ThreadLocal的beforeExecute()、afterExecute()和remove()方法,这些方法会在线程池中执行任务前后被调用,以及从线程池中移除线程时被调用。
状态保存与恢复:在线程切换之前,TransmittableThreadLocal会保存当前线程中所有TransmittableThreadLocal对象的状态或值。保存的方式是通过自动化的字节码增强操作,将相关状态存储到一个共享的数据结构中。
线程切换时的绑定与解绑操作:在线程切换之后,TransmittableThreadLocal会在新线程中恢复之前保存的值。它会根据线程的标识符(Thread ID)来获取对应的之前保存的状态。这样,新线程就能够访问到与之前线程相关的TransmittableThreadLocal对象的值。
总的来说,TransmittableThreadLocal通过使用字节码增强和拦截线程切换的方式,实现了在线程之间传递上下文的能力。通过保存和恢复线程中的TransmittableThreadLocal的状态,它确保在线程池等复杂场景下,正确地传递变量值,保持线程之间的隔离性