序文
前回TransmittableThreadLocalフレームワークの作者からコメントがあったので、ソースコードを読み直しました。最後に、この日曜日に、TransmittableThreadLocalがスレッドプール変数の喪失の問題を解決していることを知り、私の以前の理解。
InheritableThreadLocalは親スレッドと子スレッド間の変数送信の問題を解決すると思っていましたが、主にTransmittableThreadLocalがスレッドプール変数を失う問題を解決するため、これに問題はありません。親スレッドですが、スレッドプールのため、結果はスラップされました。子スレッドの最初のバッチはメインスレッドによって作成され、親スレッドと子スレッドに属します。
最も重大な問題は、スレッドプールが前のスレッドを再利用するため、親スレッドのローカル変数が更新された後、以前に作成された子スレッドがこの値を取得できないことです。
それで、それがどのように解決されるか見てみましょう〜
InheritableThreadLocalの欠陥
スレッドプール内のスレッドの最初のバッチは親スレッド変数を取得できますか?
デモを通して試してみましょう
ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,1, TimeUnit.MINUTES,new ArrayBlockingQueue<>(1));
ThreadLocal local = new InheritableThreadLocal();
local.set(1);
executor.execute(()->{
System.out.println("打印1:"+local.get());
});
印刷は1です。ThreadLocalgetメソッドは、現在のスレッドのマップを使用して値を検索します。親スレッドの値は子スレッドで見つけることができるため、スレッドプールの最初のバッチによって作成された子スレッドを意味します。親スレッドの変数にコピーされます。これはInheritableThreadLocalのクレジットです。
では、InheritableThreadLocalの欠陥はどこにありますか?
その欠陥は、実際にはTransmittableThreadLocalが解決しなければならないことです。主な問題は、スレッドプールのスレッドの再利用です。誰もがプーリングテクノロジーについて聞いたことはありますが、聞いたことはありません。接続がホットであるだけで、毎回新しい接続を取得する必要はありません。
これは、後で親スレッドを変更すると、子スレッドがローカル変数マップを更新せず、重要な問題が発生することを意味します〜
コードを見てみましょう
ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,1, TimeUnit.MINUTES,new ArrayBlockingQueue<>(1));
ThreadLocal local = new InheritableThreadLocal();
local.set(1);
executor.execute(()->{
System.out.println("打印1:"+local.get());
});
local.set(2);
System.out.println("打印2:"+local.get());
executor.execute(()->{
System.out.println("打印3:"+local.get());
});
它居然打印的还是1,我的天,就是我们刚刚讲的,父线程更新了,子线程拿到还是旧的值。
这样会引发什么问题呢?
如果我在实现apm全链路追踪的功能,我用本地变量缓存当前访问的traceid,使用线程池的话,那么我们下次请求还是会拿到旧的traceid,那就gg
解决方案是什么?
local.set(2);
System.out.println("打印2:"+local.get());
executor.execute(()->{
local.set(2);
System.out.println("打印3:"+local.get());
})
解决方案也很简单,就是在线程里头重新set一遍,为啥这样就能解决呢?
回到ThreadLocal get方法上,它是从本地线程去拿的,如果你重新去set了,那么本地线程变量也能读到了。
TransmittableThreadLocal
它如何解决线程池变量更新问题的呢?
我们来看下一个例子
private static ExecutorService TTLExecutor = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(5));
//定义另外一个线程池循环执行,模拟业务场景下多Http请求调用的情况
private static ExecutorService loopExecutor = Executors.newFixedThreadPool(5);
private static AtomicInteger i=new AtomicInteger(0);
//TTL的ThreadLocal
private static ThreadLocal tl = new TransmittableThreadLocal<>(); //这里采用TTL的实现
public static void main(String[] args) {
while (true) {
loopExecutor.execute( () -> {
if(i.get()<10){
tl.set(i.getAndAdd(1));
TTLExecutor.execute(() -> {
System.out.println(String.format("子线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));
});
}
});
}
}
它的打印是正常的,就是父线程累加数字,子线程也能正常读取,关键就这TtlExecutors.getTtlExecutorService。
ExecutorServiceTtlWrapper
这是一个封装类,把ExecutorService包进去,那它关键做了什么?
好家伙,把Runnable,callable封装了一层,然后再给线程池提交
TtlRunnable
看到了吗?最核心的来了,快照,还有Transmitter发射器
Transmitter
这个发射器里头有快照,快照保存什么呢?
我们可以想象成两个值【所有父子线程变量,子线程自身变量】
我们注意下hold这个类,是一个全局静态变量,类似一个收集者。
它的思路是怎样的呢?
我们再根据demo进行debug进去看看
com.alibaba.ttl.TtlRunnable#run
分为三部分,分别是取出旧的快照,然后把新快照塞进子线程,然后再把旧快照补回去子线程。
- 取出旧的快照
Object captured = capturedRef.get();
- 把新快照塞进子线程
原文叫重放
首先它拿到所有的变量,塞到backup里头,然后做了一次更新操作,比如说我一个子线程删除了,是不是要把hold这个统计里头剔除掉对吧
setTtlValuesTo
这个就是最重要的把父变量塞到子线程里头
- 把旧快照backup塞回子线程
为啥?因为线程复用,比如说A线程塞了一个xx,下次其实应该拿不到了,但是实际上因为线程复用导致还能拿到,所以我们需要将旧快照塞回去。
总结
TransmittableThreadLocal通过将线程封装成TtlRunnable,然后通过快照还有hold一个总收集变量东西来解决
agent无侵入实现
其实就是改写excute方法,塞入改造后的TtlRunnable,而不是之前的Runnable;
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。