你好,我是yes。
之前我已经分析了ThreadLocal、InheritableThreadLocal、FastThreadLocal。
然后有小伙伴让我再说说TransmittableThreadLocal(下边统一简称:TTL),它是阿里开源的一个工具类,解决异步执行时上下文传递的问题。
那今天就来介绍介绍 TTL,补充下 ThreadLocal 家族的短板吧。
这篇过后,ThreadLocal 就真的一网打尽了!
不过还是建议先看看前置篇(文末会放链接),不然理解起来可能有点困难。
缘由
任何一个组件的出现必有其缘由,知其缘由背景才能更深刻地理解它。
我们知道 ThreadLocal 的出现就是为了本地化线程资源,防止不必要的多线程之间的竞争。
在有些场景,当父线程 new 一个子线程的时候,希望把它的 ThreadLocal 继承给子线程,这时候 InheritableThreadLocal 就来了,它就是为了父子线程传递本地化资源而提出的。
具体的实现是在子线程对象被 new 的时候,即 Thread.init 的时,如果查看到父线程内部有 InheritableThreadLocal 的数据,那就在子 Thread 初始化的时,把父线程的 InheritableThreadLocal 拷贝给子线程。
就这样简单地把父线程的 ThreadLocal 数据传递给子线程了。
但是,这个场景只能发生在 new Thread 的时候!也就是手动创建线程之时!那就有个问题了,在平时我们使用的时候基本用的都是线程池。
那就麻了啊,线程池里面的线程都预创建好了,调用的时候就没法直接用 InheritableThreadLocal 了。
所以就产生了一个需求,**如何往线程池内的线程传递 ThreadLocal? **,JDK 的类库没这个功能,所以怎么搞?
只能我们自己造轮子了。
如何设计?
需求已经明确了,但是怎么实现呢?
平时我们用线程池的话,比如你要提交任务,则使用代码如下:
Runnable task = new Runnable....; executorService.submit(task);
小贴士:以下的 ThreadLocal 泛指线程本地数据,不是指 ThreadLocal 这个类
这时候,我们想着把当前线程的 ThreadLocal 传递给线程池内部将要执行这个 task 的线程,但此时我们哪知道线程池里面的哪个线程会来执行这个任务?
所以,我们得先把当前线程的 ThreadLocal 保存到这个 task 中,然后当线程池里的某个线程,比如线程 A 获取这个任务要执行的时候,看看 task 里面是否有存储着的 ThreadLocal ,如果存着那就把这个 ThreadLocal 放到线程 A 的本地变量里,这样就完成了传递。
然后还有一步,也挺关键的,就是恢复线程池内部执行线程的上下文,也就是该任务执行完毕之后,把任务带来的本地数据给删了,把线程以前的本地数据复原。
设计思路应该已经很明确了吧?来看看具体需要如何实现吧!
如何实现?
把上面的设计简单地、直白地翻译成代码如下:
如果你读过我之前分析 ThreadLocal 的文章,应该可以很容易的理解上面的操作。
这样虽然可以实现,但是可操作性太差,耦合性太高。
所以我们得想想怎么优化一下,其实有个设计模式就很合适,那就是装饰器模式。
我们可以自己搞一个 Runnable 类,比如 YesRunnable,然后在 new YesRunnable 的时候,在构造器里面把当前线程的 threadlocal 赋值进去。
然后 run 方法那里也修饰一下,我们直接看看伪代码:
public YesRunnable(Runnable runable) { this.threadlocalCopy = copyFatherThreadlocal(); this.runable = runable; } public void run() { //塞入父threadlocal,并返回当前线程原先threadlocal Object backup = setThreadlocal(threadlocalCopy); try { runable.run();//执行被装饰的任务逻辑 } finally { restore(backup) ; //复原当前线程的上下文 } }
使用方式如下:
Runnable task = () -> {...}; YesRunnable yesRunnable = new YesRunnable(task); executorService.submit(yesRunnable);
你看,这不就实现我们上面的设计了嘛!
不过还有一个点没有揭秘,就是如何实现 copyFatherThreadlocal
。
我们如何得知父线程现在到底有哪些 Threadlocal?并且哪些是需要上下文传递的?
所以我们还需要创建一个类来继承 Threadlocal,比如叫 YesThreadlocal,用它声明的变量就表明需要父子传递的!
public class YesThreadlocal<T> extends ThreadLocal<T>
然后我们需要搞个地方来存储当前父线程上下文用到的所有 YesThreadlocal,这样在 copyFatherThreadlocal
的时候我们才好遍历复制对吧?
我们可以搞个 holder 来保存这些 YesThreadlocal ,不过 holder 变量也得线程隔离,毕竟每个线程所要使用的 YesThreadlocal 都不一样,所以需要用 ThreadLocal 来修饰。
然后 YesThreadlocal 可能会有很多,我们可以用 set 来保存,但是为了防止我们搞的这个 holder 造成内存泄漏的风险,我们需要弱引用它,不过没有 WeakHashSet,那我们就用 WeakHashMap 来替代存储。
private static final ThreadLocal<WeakHashMap<YesThreadLocal<Object>, ?>> holder = new .....
这样我们就打造了一个变量,它是线程独有的,且又能拿来存储当前线程用到的所有 YesThreadLocal ,便于后面的复制,且又不会造成内存泄漏(弱引用)。
是不是感觉有点暂时理不清?没事,我们继续来看看具体怎么用上这个 hold ,可能会清晰些。
首先我们将需要传递给线程池的本地变量从 ThreadLocal 替换成 YesThreadLocal。
然后重写 set 方法,实现如下:
@Override public final void set(T value) { super.set(value); //调用 ThreadLocal 的 set addThisToHolder(); // 把当前的 YesThreadLocal 对象塞入 hold 中。 } private void addThisToHolder() { if (!holder.get().containsKey(this)) { holder.get().put((YesThreadLocal <Object>) this, null); } }
你看这样就把所有用到的 YesThreadLocal 塞到 holder 中了,然后再来看看 copyFatherThreadlocal 应该如何实现。
private static HashMap<YesThreadLocal <Object>, Object> copyFatherThreadlocal () { HashMap<YesThreadLocal <Object>, Object> fatherMap = new HashMap<YesThreadLocal <Object>, Object>(); for (YesThreadLocal <Object> threadLocal : YesThreadLocal.holder.get().keySet()) { fatherMap .put(threadLocal, threadLocal.copyValue()); } return fatherMap ; }
逻辑很简单,就是一个 map 遍历拷贝。
我现在用一段话来小结一下,把上面的全部操作联合起来理解,应该会清晰很多。