关于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线程复用问题内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring Boot利用Lombok减少Java中样板代码的方法示例

    Spring Boot利用Lombok减少Java中样板代码的方法示例

    spring Boot是非常高效的开发框架,lombok是一套代码模板解决方案,将极大提升开发的效率,下面这篇文章主要给大家介绍了关于Spring Boot利用Lombok减少Java中样板代码的相关资料,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-09-09
  • Springboot详解整合SpringSecurity实现全过程

    Springboot详解整合SpringSecurity实现全过程

    Spring Security基于Spring开发,项目中如果使用Springboot作为基础,配合Spring Security做权限更加方便,而Shiro需要和Spring进行整合开发。因此作为spring全家桶中的Spring Security在java领域很常用
    2022-07-07
  • SpringBoot整合Ip2region获取IP地址和定位的详细过程

    SpringBoot整合Ip2region获取IP地址和定位的详细过程

    ip2region v2.0 - 是一个离线IP地址定位库和IP定位数据管理框架,10微秒级别的查询效率,提供了众多主流编程语言的 xdb 数据生成和查询客户端实现 ,这篇文章主要介绍了SpringBoot整合Ip2region获取IP地址和定位,需要的朋友可以参考下
    2023-06-06
  • Java的静态方法Arrays.asList()使用指南

    Java的静态方法Arrays.asList()使用指南

    Arrays.asList() 是一个 Java 的静态方法,它可以把一个数组或者多个参数转换成一个 List 集合,这个方法可以作为数组和集合之间的桥梁,方便我们使用集合的一些方法和特性,本文将介绍 Arrays.asList() 的语法、应用场景、坑点和总结
    2023-09-09
  • Java每7天日志自动清理的项目实践

    Java每7天日志自动清理的项目实践

    在实际项目中由于服务器内存有限,人工清理常会忘记,本文主要介绍了Java每7天日志自动清理的项目实践,具有一定的参考价值,感兴趣的可以了解一下
    2024-01-01
  • java 文件目录读写删除操作详细实现代码

    java 文件目录读写删除操作详细实现代码

    这篇文章主要介绍了java 文件读写删操作详细实现代码,需要的朋友可以参考下
    2017-09-09
  • java中set集合的常用方法详解

    java中set集合的常用方法详解

    本篇文章给大家带来的内容是关于java中set集合的常用方法详解,有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。下面我们就来学习一下吧
    2021-11-11
  • 详解java倒计时三种简单实现方式

    详解java倒计时三种简单实现方式

    这篇文章主要介绍了详解java倒计时三种简单实现方式,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09
  • Spring的自动装配Bean的三种方式

    Spring的自动装配Bean的三种方式

    本篇文章主要介绍了 Spring的自动装配Bean的三种方式,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-02-02
  • Java Optional用法面试题精讲

    Java Optional用法面试题精讲

    这篇文章主要为大家介绍了Java Optional用法面试题精讲,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-09-09

最新评论