关于TransmittableThreadLocal线程池中线程复用问题的解决方案

 更新时间:2023年11月15日 09:36:06   作者:立小研先森  
这篇文章主要介绍了关于TransmittableThreadLocal线程池中线程复用问题的解决方案,线程池复用线程,如果子线程执行完未移除上下文,则会导致后续线程可以取到之前线程设置的属性,需要的朋友可以参考下

TransmittableThreadLocal线程复用问题

TTL对线程或者线程池的核心通过装饰器模式做了处理,核心如下:

  • capture:获取父线程中的值,包括引用对象或普通对象
  • replay:回放:备份、将父线程的值设置到子线程
  • restore:将子线程执行之前backup的值设置回子线程的ThreadLocal

一、TTL在线程池场景

线程池复用线程,如果子线程执行完未移除上下文,则会导致后续线程可以取到之前线程设置的属性

public class TtlErrorTest {
    private static final TransmittableThreadLocal<User> context = new TransmittableThreadLocal<>();
    
    private static final ExecutorService service = Executors.newSingleThreadExecutor();
  //TTL修饰过的线程池(1)
//  private static final Executor service = TtlExecutors.getTtlExecutor(Executors.newSingleThreadExecutor());

    public static void main(String[] args) {
      	//主线程设置上下文变量(3)
        //User user1 = new User();
        //user1.setUsername("孙少平");
        //user1.setPassword("123456");
        //context.set(user1);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                User user = new User();
                user.setUsername("田晓霞");
                context.set(user);
                System.out.println("1-" + Thread.currentThread().getName() + ":" + context.get());
                //移除上下文变量(2)
                //context.remove();
            }
        };
        Runnable runnable1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("2-" + Thread.currentThread().getName() + ":" + context.get());
            }
        };
        service.execute(runnable);
        service.execute(runnable1);
       //移除上下文(4)
        //ntext.remove();
    }
}

结果一:

1-pool-1-thread-1:田晓霞
2-pool-1-thread-1:田晓霞

结果二(将代码中的(1)(3)打开运行):

1-pool-1-thread-1:田晓霞
2-pool-1-thread-1:孙少平

分析:上述案例定义了只有一个工作线程执行队列中线程的线程池,这样做是为了立马复现线程复用的效果;第一个线程中第一了上下文值引用对象,第二个线程之中没有定义,效果是第二个线程获取到了第一个线程之中设置的上下文变量;如果将第一个线程中(2)注释打开,则第二个线程获取不到第一个线程中设置的上下文变量;

如果将(1)打开会是什么效果,效果是第二个线程拿不到第一个线程中设置的上下文值,这又是为什么呢?在第一个线程中又没有主动remove掉为何使用了TTL修饰的线程池就拿不到符合预期了呢?

如果仔细分析源码会发现TTL修饰的线程使用装饰器模式,第一步capture捕获主线程上下文,第二部replay回放、backup备份子线程上下文、将主线程上下文值设置到子线程,第三部restore恢复阶段、将backup备份恢复到子线程上下文,因为子线程上下文执行没有任何值,restore恢复后上下文是干净的,上下文没有任何值;那在第二个线程复用线程的时候是取不到指的,符合预期;

如果将(1)(3)打开运行结果也是符合预期的,这里面有个问题,无论线程一、线程二执行完成后线程池中复用的线程都会持有一个User对象的引用,这样GC的时候就无法回收资源,如果线程池持有的引用对象足够多,引用对象占用的内存足够大的时候就有可能引发OOM异常;

如果将代码(4)打开则会移除主线程的上下文,但是子线程从父线程继承的上下文属性是无法移除的;

二、TTL如何保证线程池中复用的线程不持有其它线程的属性值

针对上述线程池复用可能导致内存OOM的问题提供了两种解决方案

方案一:通过TtlExecutors.getDefaultDisableInheritableThreadFactory()在线程池中创建禁止继承父线程上下文的线程

public class TtlFactoryTest {
    private static final TransmittableThreadLocal<User> context = new TransmittableThreadLocal<>();
    static final ExecutorService service = Executors.newFixedThreadPool(2, TtlExecutors.getDefaultDisableInheritableThreadFactory());
    public static void main(String[] args) {
        User user = new User();
        user.setUsername("田晓霞");
        context.set(user);
        System.out.println("1-" + Thread.currentThread().getName() + "-" + context.get());
        service.submit(new Runnable() {
            @Override
            public void run() {
                User user1 = new User();
                user1.setUsername("田二");
                context.set(user1);
                System.out.println("2-" + Thread.currentThread().getName() + "-" + context.get());
            }
        });
        System.out.println("3-" + Thread.currentThread().getName() + "-" + context.get());
    }
}

方案二:去除掉父子线程的继承关系,相当于TTL由InheritableThreadLocal回退到了ThreadLocal

TransmittableThreadLocal<String> t1 = new TransmittableThreadLocal<String>() {
    protected String childValue(String parentValue) {
        return initialValue();
    }
}

此方法是去除掉父子线程之间的继承关系,会将TTL回退到ThreadLocal,对于非线程池的父子线程继承关系是不适用的;

到此这篇关于关于TransmittableThreadLocal线程池中线程复用问题的解决方案的文章就介绍到这了,更多相关TransmittableThreadLocal线程复用问题内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • JavaWeb详细讲述Cookie和Session的概念

    JavaWeb详细讲述Cookie和Session的概念

    web开发阶段我们主要是浏览器和服务器之间来进行交互。浏览器和服务器之间的交互就像人和人之间进行交流一样,但是对于机器来说,在一次请求之间只是会携带着本次请求的数据的,但是可能多次请求之间是会有联系的,所以提供了会话机制
    2022-06-06
  • Java多线程死锁示例

    Java多线程死锁示例

    这篇文章主要介绍了Java多线程死锁,结合实例形式分析了Java多线程出现死锁的相关原因与操作注意事项,需要的朋友可以参考下
    2018-08-08
  • Spring实例化bean的方式大全

    Spring实例化bean的方式大全

    Spring 实例化 Bean 主要有三种核心方式:构造器实例化、静态工厂方法实例化和实例工厂方法实例化,其中构造器实例化是最常用的方式,下面小编给大家详细说说这几种方式,需要的朋友可以参考下
    2025-09-09
  • 基于@PathVariable注解的用法说明

    基于@PathVariable注解的用法说明

    这篇文章主要介绍了基于@PathVariable注解的用法说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • Java String类的常用方法汇总

    Java String类的常用方法汇总

    这篇文章主要为大家详细汇总了Java String类的常用方法,感兴趣的小伙伴们可以参考一下
    2016-07-07
  • ssm框架+PageHelper插件实现分页查询功能

    ssm框架+PageHelper插件实现分页查询功能

    今天小编教大家如何通过ssm框架+PageHelper插件实现分页查询功能,首先大家需要新建一个maven工程引入jar包,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2021-06-06
  • 详解Java实践之抽象工厂模式

    详解Java实践之抽象工厂模式

    抽象工厂模式用于产品族的构建。抽象工厂是所有形态的工厂模式中最为抽象和最具一般性的一种形态。抽象工厂是指当有多个抽象角色时使用的一种工厂模式。抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体情况下,创建多个产品族中的产品对象
    2021-06-06
  • Java使用jacob将微软office中word、excel、ppt转成pdf

    Java使用jacob将微软office中word、excel、ppt转成pdf

    这篇文章主要为大家详细介绍了Java使用jacob将微软office中word、excel、ppt转成pdf,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-12-12
  • Java 8中字符串拼接新姿势StringJoiner详解

    Java 8中字符串拼接新姿势StringJoiner详解

    在本篇文章里小编给大家整理了关于Java 8中字符串拼接新姿势StringJoiner的详解内容,需要的朋友们参考下。
    2019-09-09
  • Java Spring三级缓存

    Java Spring三级缓存

    三级缓存就是在Bean生成流程中保存Bean对象三种形态的三个Map集合,这个三级缓存就是为了解决循环依赖,本文给大家介绍Java Spring三级缓存,感兴趣的朋友一起看看吧
    2025-06-06

最新评论