searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

使用 TransmittableThreadLocal(TTL)解决异步子线程与主线程的变量获取问题

2024-05-29 09:08:34
23
0

ThreadLocal是可以在一个线程中当容器使用的局部变量,是线程隔离、线程安全的。

如果子线程要获取父线程的变量,会不太方便。比方在业务代码中,为了提高响应速度,将多个复杂、长时间的计算或调用过程异步进行,让主线程可以先进行其他操作。而异步子线程此时可能可能会用到主线程ThreadLocal中的内容,如链路追踪。

1.ThreadLocal

public static void main(String[] args) throws InterruptedException {
         ThreadLocal<String> parent = new ThreadLocal<>();
         parent.set(Thread.currentThread().getName() + "=======hello,myThread00");
 ​
         new Thread(() -> {
             try {
                 // 设置本线程变量
                 parent.set(Thread.currentThread().getName() + "=======hello,myThread01");
                 // dosomething
                 Thread.sleep(3000);
                 // 使用线程变量
                 System.out.println(Thread.currentThread().getName() + ":" + parent.get());
                 // 清除
                 parent.remove();
                 // do other thing
                 //.....
             } catch (Exception e) {
                 e.printStackTrace();
             }
         }, "thread-1").start();
 ​
         new Thread(() -> {
             try {
                 // 设置本线程变量
                 parent.set(Thread.currentThread().getName() + "=======hello,myThread02");
                 // dosomething
                 Thread.sleep(4000);
                 // 使用线程变量
                 System.out.println(Thread.currentThread().getName() + ":" + parent.get());
                 // 清除
                 parent.remove();
                 // do other thing
                 //.....
             } catch (Exception e) {
                 e.printStackTrace();
             }
         }, "thread-2").start();
 ​
         System.out.println(Thread.currentThread().getName() + ":" + parent.get());
     }

运行结果:
main:main=======hello,myThread00
thread-1:thread-1=======hello,myThread01
thread-2:thread-2=======hello,myThread02

探究:

子线程能不能直接获取到父线程的变量?

public static void main(String[] args) throws InterruptedException {
         ThreadLocal<String> parent = new ThreadLocal<>();
         parent.set(Thread.currentThread().getName() + "=======hello,myThread");
         new Thread(() -> {
             try {
                 // 使用线程变量
                 System.out.println(Thread.currentThread().getName() + ":" + parent.get());
                 // do other thing
                 // .....
             } catch (Exception e) {
                 e.printStackTrace();
             }
         }, "child-thread").start();
     }

运行结果:
child-thread:null

即子线程拿不到父线程的变量。

二、使用InheritableThreadLocal
2.1、ITL可用的情况:
JDK提供了InheritableThreadLocal可以让子线程拿到父线程的变量:

示例:

public static void main(String[] args) throws InterruptedException {
         ThreadLocal<String> parent = new InheritableThreadLocal<>();
         parent.set(Thread.currentThread().getName() + "=======hello,myThread");
 ​
     new Thread(() -> {
         try {
             // 使用线程变量
             System.out.println(Thread.currentThread().getName() + ":" + parent.get());
             // do other thing
             // .....
         } catch (Exception e) {
             e.printStackTrace();
         }
     }, "child-thread").start();
 }

运行结果:
child-thread:main=======hello,myThread

即子线程中拿到了父线程的值。

2.2、ITL不可用的情况:
InheritableThreadLocal的继承性是在new Thread创建子线程时候在构造函数内把父线程内线程变量拷贝到子线程内部的。

为了不在创建新线程耗费资源,我们一般会用线程池,而线程池的线程会复用,那么线程中的ThreadLocal便不对了,因为线程可能是旧的。

如下代码:代码2的部分向线程池投递三个任务,这时候线程池内两个核心线程会被创建,并且队列里面有被放入一个元素。然后代码3休眠4s,旨在让线程池避免饱和执行拒绝策略,然后代码4设置线程变量,代码5提交任务到线程池。

private static final ThreadPoolExecutor bizPoolExecutor = new ThreadPoolExecutor
             (2, 2, 1, MINUTES,
             new LinkedBlockingQueue<>(1));
 ​
 public static void main(String[] args) throws InterruptedException {
     // 1 创建线程变量
     InheritableThreadLocal<String> parent = new InheritableThreadLocal<>();
     // 2 投递三个任务,让线程池中的线程全部创建。
     for (int i = 0; i < 3; ++i) {
         bizPoolExecutor.execute(() -> {
             try {
                 Thread.sleep(3000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         });
     }
     // 3休眠4s
     Thread.sleep(4000);
     // 4.设置线程变量
     parent.set("value-set-in-parent");
 ​
     // 5. 提交任务到线程池
     bizPoolExecutor.execute(() -> {
         try {
             // 5.1访问线程变量
             System.out.println("parent:" + parent.get());
         } catch (Exception e) {
             e.printStackTrace();
         }
     });
 }

运行结果:

parent:null

即子线程内访问不到父线程设置变量

如果没有2那段代码,则可以拿到结果:

parent:value-set-in-parent

2.3、ITL原理:
ThreadLocal 实际上是 Thread 中保存的一个 ThreadLocalMap 类型的属性,与Thread搭配使用。

InheritableThreadLocal 也是如此。

public class Thread implements Runnable {
     // 如果单纯使用 ThreadLocal,则 Thread 使用该属性值保存 ThreadLocalMap
     ThreadLocal.ThreadLocalMap threadLocals = null;
     // 否则使用该属性值
     ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 ​
 //inheritThreadLocals默认true
     private void init(ThreadGroup g, Runnable target, String name,
                   long stackSize, AccessControlContext acc,
                   boolean inheritThreadLocals) {
     Thread parent = currentThread();
       …………
     if (inheritThreadLocals && parent.inheritableThreadLocals != null)
         this.inheritableThreadLocals =
         //Thread的init方法创建新线程时把父线程的拿了过来
         ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
 ​
 }

InheritableThreadLocal是ThreadLocal的子类,复写了几个方法:

public class InheritableThreadLocal extends ThreadLocal {
protected T childValue(T parentValue) {
     return parentValue;
 }
  
 ThreadLocalMap getMap(Thread t) {
    return t.inheritableThreadLocals;
 }
  
 void createMap(Thread t, T firstValue) {
     t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
 }
 }

ThreadLocal中对应的方法:

public class ThreadLocal<T> {
 ​
    T childValue(T parentValue) {
        throw new UnsupportedOperationException();
    }
 ​
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
 ​
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
 }

InheritableThreadLocal通过复写方法,主线程 new InheritableThreadLocal(),使Thread中的 inheritableThreadLocals 不为空,且 getMap 获取 ThreadLocalMap时返回 inheritableThreadLocals。childValue 方法用作从父线程中获取值。

去 new Thread 创建子线程时,主线程当前线程的 parent.inheritableThreadLocals != null,且将主线程的拿了过来。

三、TransmittableThreadLocal
3.1、TTL使用:
添加pom:

<dependency>
             <groupId>com.alibaba</groupId>
             <artifactId>transmittable-thread-local</artifactId>
             <version>2.12.0</version>
         </dependency>

示例:1和5处代码变化,添加6。把具体任务使用TtlRunnable.get(task)包装了下,然后再提交到线程池。

public class Ttl {
 // 0.创建线程池
 private static final ThreadPoolExecutor bizPoolExecutor =
         new ThreadPoolExecutor(2, 2, 1, MINUTES,
         new LinkedBlockingQueue<>(1));
 ​
     public static void main(String[] args) throws InterruptedException {
 ​
         // 1 创建线程变量
         ThreadLocal<String> parent = new TransmittableThreadLocal<>();
 ​
         // 2 投递三个任务,让线程池中的线程全部创建。
         for (int i = 0; i < 3; ++i) {
             bizPoolExecutor.execute(() -> {
                 try {
                     Thread.sleep(3000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             });
         }
         // 3休眠4s
         Thread.sleep(4000);
         // 4.设置线程变量
         parent.set("value-set-in-parent");
 ​
         // 5. 提交任务到线程池
         Runnable task = () -> {
             try {
                 // 5.1访问线程变量
                 System.out.println("parent:" + parent.get());
             } catch (Exception e) {
                 e.printStackTrace();
             }
         };
 ​
         // 6、额外的处理,生成修饰了的对象ttlRunnable
         Runnable ttlRunnable = TtlRunnable.get(task);
         bizPoolExecutor.execute(ttlRunnable);
     }
 }

运行结果:

parent:value-set-in-parent

TransmittableThreadLocal 完美解决了线程变量继承问题

3.2、TTL解析:
在线程池场景下我们的需求:

InheritableThreadLocal 的是在 new Thread 时候把父线程的 inheritableThreadLocals 复制到了子线程,从而实现线程变量的继承特性。

而现在我们需要在提交任务到线程池前,把父线程中的线程变量保存到任务内,然后等线程池内线程执行任务前把保存的父线程的线程变量复制到线程池中的执行线程上,然后运行我们的任务,等任务运行完毕后,在清除掉执行线程上的线程变量。

可知第一我们需要包装提交到线程池内的任务,里面添加一个变量来保存父线程的线程变量。TransmittableThreadLocal 就是这样的思路。

如上代码6就是把我们的任务使用 TtlRunnable.get(task) 包装了,其内部就是拷贝父线程中的线程变量到包装的任务内保存起来。TtlRunnable.get(task)的get方法最后调到了:

private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {  
     this.capturedRef = new AtomicReference<Object>(capture());  
     this.runnable = runnable;   
     this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;   
 }

原理:

1、TTL 继承自 InheritableThreadLocal。
2、通过一个 Holder,保存了每个线程当前持有的所有 ThreadLocal 对象。
3、用 TtlRunnable 的 get 方法来包裹一个 Runnable 对象,包裹对象时,会采用类似 SNAPSHOT,快照的机制,通过 Holder,捕获父线程当前持有的所有ThreadLocal。随后,子线程启动,在Runnable对象执行run方法之前,从Holder中取出先前捕获到的父线程所持有的ThreadLocal对象,并设置到当前子线程当中,设置之前会保存子线程原有的ThreadLocal作为backUp,当子线程执行结束后,通过backUp恢复其原有的ThreadLocal。

0条评论
0 / 1000
lihj21
1文章数
0粉丝数
lihj21
1 文章 | 0 粉丝
lihj21
1文章数
0粉丝数
lihj21
1 文章 | 0 粉丝
原创

使用 TransmittableThreadLocal(TTL)解决异步子线程与主线程的变量获取问题

2024-05-29 09:08:34
23
0

ThreadLocal是可以在一个线程中当容器使用的局部变量,是线程隔离、线程安全的。

如果子线程要获取父线程的变量,会不太方便。比方在业务代码中,为了提高响应速度,将多个复杂、长时间的计算或调用过程异步进行,让主线程可以先进行其他操作。而异步子线程此时可能可能会用到主线程ThreadLocal中的内容,如链路追踪。

1.ThreadLocal

public static void main(String[] args) throws InterruptedException {
         ThreadLocal<String> parent = new ThreadLocal<>();
         parent.set(Thread.currentThread().getName() + "=======hello,myThread00");
 ​
         new Thread(() -> {
             try {
                 // 设置本线程变量
                 parent.set(Thread.currentThread().getName() + "=======hello,myThread01");
                 // dosomething
                 Thread.sleep(3000);
                 // 使用线程变量
                 System.out.println(Thread.currentThread().getName() + ":" + parent.get());
                 // 清除
                 parent.remove();
                 // do other thing
                 //.....
             } catch (Exception e) {
                 e.printStackTrace();
             }
         }, "thread-1").start();
 ​
         new Thread(() -> {
             try {
                 // 设置本线程变量
                 parent.set(Thread.currentThread().getName() + "=======hello,myThread02");
                 // dosomething
                 Thread.sleep(4000);
                 // 使用线程变量
                 System.out.println(Thread.currentThread().getName() + ":" + parent.get());
                 // 清除
                 parent.remove();
                 // do other thing
                 //.....
             } catch (Exception e) {
                 e.printStackTrace();
             }
         }, "thread-2").start();
 ​
         System.out.println(Thread.currentThread().getName() + ":" + parent.get());
     }

运行结果:
main:main=======hello,myThread00
thread-1:thread-1=======hello,myThread01
thread-2:thread-2=======hello,myThread02

探究:

子线程能不能直接获取到父线程的变量?

public static void main(String[] args) throws InterruptedException {
         ThreadLocal<String> parent = new ThreadLocal<>();
         parent.set(Thread.currentThread().getName() + "=======hello,myThread");
         new Thread(() -> {
             try {
                 // 使用线程变量
                 System.out.println(Thread.currentThread().getName() + ":" + parent.get());
                 // do other thing
                 // .....
             } catch (Exception e) {
                 e.printStackTrace();
             }
         }, "child-thread").start();
     }

运行结果:
child-thread:null

即子线程拿不到父线程的变量。

二、使用InheritableThreadLocal
2.1、ITL可用的情况:
JDK提供了InheritableThreadLocal可以让子线程拿到父线程的变量:

示例:

public static void main(String[] args) throws InterruptedException {
         ThreadLocal<String> parent = new InheritableThreadLocal<>();
         parent.set(Thread.currentThread().getName() + "=======hello,myThread");
 ​
     new Thread(() -> {
         try {
             // 使用线程变量
             System.out.println(Thread.currentThread().getName() + ":" + parent.get());
             // do other thing
             // .....
         } catch (Exception e) {
             e.printStackTrace();
         }
     }, "child-thread").start();
 }

运行结果:
child-thread:main=======hello,myThread

即子线程中拿到了父线程的值。

2.2、ITL不可用的情况:
InheritableThreadLocal的继承性是在new Thread创建子线程时候在构造函数内把父线程内线程变量拷贝到子线程内部的。

为了不在创建新线程耗费资源,我们一般会用线程池,而线程池的线程会复用,那么线程中的ThreadLocal便不对了,因为线程可能是旧的。

如下代码:代码2的部分向线程池投递三个任务,这时候线程池内两个核心线程会被创建,并且队列里面有被放入一个元素。然后代码3休眠4s,旨在让线程池避免饱和执行拒绝策略,然后代码4设置线程变量,代码5提交任务到线程池。

private static final ThreadPoolExecutor bizPoolExecutor = new ThreadPoolExecutor
             (2, 2, 1, MINUTES,
             new LinkedBlockingQueue<>(1));
 ​
 public static void main(String[] args) throws InterruptedException {
     // 1 创建线程变量
     InheritableThreadLocal<String> parent = new InheritableThreadLocal<>();
     // 2 投递三个任务,让线程池中的线程全部创建。
     for (int i = 0; i < 3; ++i) {
         bizPoolExecutor.execute(() -> {
             try {
                 Thread.sleep(3000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         });
     }
     // 3休眠4s
     Thread.sleep(4000);
     // 4.设置线程变量
     parent.set("value-set-in-parent");
 ​
     // 5. 提交任务到线程池
     bizPoolExecutor.execute(() -> {
         try {
             // 5.1访问线程变量
             System.out.println("parent:" + parent.get());
         } catch (Exception e) {
             e.printStackTrace();
         }
     });
 }

运行结果:

parent:null

即子线程内访问不到父线程设置变量

如果没有2那段代码,则可以拿到结果:

parent:value-set-in-parent

2.3、ITL原理:
ThreadLocal 实际上是 Thread 中保存的一个 ThreadLocalMap 类型的属性,与Thread搭配使用。

InheritableThreadLocal 也是如此。

public class Thread implements Runnable {
     // 如果单纯使用 ThreadLocal,则 Thread 使用该属性值保存 ThreadLocalMap
     ThreadLocal.ThreadLocalMap threadLocals = null;
     // 否则使用该属性值
     ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 ​
 //inheritThreadLocals默认true
     private void init(ThreadGroup g, Runnable target, String name,
                   long stackSize, AccessControlContext acc,
                   boolean inheritThreadLocals) {
     Thread parent = currentThread();
       …………
     if (inheritThreadLocals && parent.inheritableThreadLocals != null)
         this.inheritableThreadLocals =
         //Thread的init方法创建新线程时把父线程的拿了过来
         ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
 ​
 }

InheritableThreadLocal是ThreadLocal的子类,复写了几个方法:

public class InheritableThreadLocal extends ThreadLocal {
protected T childValue(T parentValue) {
     return parentValue;
 }
  
 ThreadLocalMap getMap(Thread t) {
    return t.inheritableThreadLocals;
 }
  
 void createMap(Thread t, T firstValue) {
     t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
 }
 }

ThreadLocal中对应的方法:

public class ThreadLocal<T> {
 ​
    T childValue(T parentValue) {
        throw new UnsupportedOperationException();
    }
 ​
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
 ​
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
 }

InheritableThreadLocal通过复写方法,主线程 new InheritableThreadLocal(),使Thread中的 inheritableThreadLocals 不为空,且 getMap 获取 ThreadLocalMap时返回 inheritableThreadLocals。childValue 方法用作从父线程中获取值。

去 new Thread 创建子线程时,主线程当前线程的 parent.inheritableThreadLocals != null,且将主线程的拿了过来。

三、TransmittableThreadLocal
3.1、TTL使用:
添加pom:

<dependency>
             <groupId>com.alibaba</groupId>
             <artifactId>transmittable-thread-local</artifactId>
             <version>2.12.0</version>
         </dependency>

示例:1和5处代码变化,添加6。把具体任务使用TtlRunnable.get(task)包装了下,然后再提交到线程池。

public class Ttl {
 // 0.创建线程池
 private static final ThreadPoolExecutor bizPoolExecutor =
         new ThreadPoolExecutor(2, 2, 1, MINUTES,
         new LinkedBlockingQueue<>(1));
 ​
     public static void main(String[] args) throws InterruptedException {
 ​
         // 1 创建线程变量
         ThreadLocal<String> parent = new TransmittableThreadLocal<>();
 ​
         // 2 投递三个任务,让线程池中的线程全部创建。
         for (int i = 0; i < 3; ++i) {
             bizPoolExecutor.execute(() -> {
                 try {
                     Thread.sleep(3000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             });
         }
         // 3休眠4s
         Thread.sleep(4000);
         // 4.设置线程变量
         parent.set("value-set-in-parent");
 ​
         // 5. 提交任务到线程池
         Runnable task = () -> {
             try {
                 // 5.1访问线程变量
                 System.out.println("parent:" + parent.get());
             } catch (Exception e) {
                 e.printStackTrace();
             }
         };
 ​
         // 6、额外的处理,生成修饰了的对象ttlRunnable
         Runnable ttlRunnable = TtlRunnable.get(task);
         bizPoolExecutor.execute(ttlRunnable);
     }
 }

运行结果:

parent:value-set-in-parent

TransmittableThreadLocal 完美解决了线程变量继承问题

3.2、TTL解析:
在线程池场景下我们的需求:

InheritableThreadLocal 的是在 new Thread 时候把父线程的 inheritableThreadLocals 复制到了子线程,从而实现线程变量的继承特性。

而现在我们需要在提交任务到线程池前,把父线程中的线程变量保存到任务内,然后等线程池内线程执行任务前把保存的父线程的线程变量复制到线程池中的执行线程上,然后运行我们的任务,等任务运行完毕后,在清除掉执行线程上的线程变量。

可知第一我们需要包装提交到线程池内的任务,里面添加一个变量来保存父线程的线程变量。TransmittableThreadLocal 就是这样的思路。

如上代码6就是把我们的任务使用 TtlRunnable.get(task) 包装了,其内部就是拷贝父线程中的线程变量到包装的任务内保存起来。TtlRunnable.get(task)的get方法最后调到了:

private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {  
     this.capturedRef = new AtomicReference<Object>(capture());  
     this.runnable = runnable;   
     this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;   
 }

原理:

1、TTL 继承自 InheritableThreadLocal。
2、通过一个 Holder,保存了每个线程当前持有的所有 ThreadLocal 对象。
3、用 TtlRunnable 的 get 方法来包裹一个 Runnable 对象,包裹对象时,会采用类似 SNAPSHOT,快照的机制,通过 Holder,捕获父线程当前持有的所有ThreadLocal。随后,子线程启动,在Runnable对象执行run方法之前,从Holder中取出先前捕获到的父线程所持有的ThreadLocal对象,并设置到当前子线程当中,设置之前会保存子线程原有的ThreadLocal作为backUp,当子线程执行结束后,通过backUp恢复其原有的ThreadLocal。

文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0