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

相关文章

  • Java利用Socket实现网络通信功能

    Java利用Socket实现网络通信功能

    在早期的网络编程中,Socket是很常见的实现技术之一,比如早期的聊天室,就是基于这种技术进行实现的,另外现在有些消息推送,也可以基于Socket实现,本文小编给大家介绍了Java利用Socket实现网络通信功能的示例,需要的朋友可以参考下
    2023-11-11
  • SpringMVC 整合SSM框架详解

    SpringMVC 整合SSM框架详解

    这篇文章主要介绍了SpringMVC 整合SSM框架详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-08-08
  • 谈谈Java中自定义注解及使用场景

    谈谈Java中自定义注解及使用场景

    这篇文章主要介绍了谈谈Java中自定义注解及使用场景,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09
  • IDEA2022创建Maven Web项目教程(图文)

    IDEA2022创建Maven Web项目教程(图文)

    本文主要介绍了IDEA2022创建Maven Web项目教程,文中通过图文介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-07-07
  • 使用BigDecimal去掉小数点后无用的0

    使用BigDecimal去掉小数点后无用的0

    这篇文章主要介绍了使用BigDecimal去掉小数点后无用的0操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08
  • 一文带你掌握Java中的HashSet

    一文带你掌握Java中的HashSet

    HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合,HashSet 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的,本文将带你详细了解Java中的HashSet,,需要的朋友可以参考下
    2023-05-05
  • 基于SpringBoot加载Mybatis的TypeAlias问题

    基于SpringBoot加载Mybatis的TypeAlias问题

    这篇文章主要介绍了解决SpringBoot加载Mybatis的TypeAlias问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • Java发送post方法详解

    Java发送post方法详解

    这篇文章主要介绍了Java发送post方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-04-04
  • Java日常练习题,每天进步一点点(52)

    Java日常练习题,每天进步一点点(52)

    下面小编就为大家带来一篇Java基础的几道练习题(分享)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧,希望可以帮到你
    2021-08-08
  • Springboot2.1.6集成activiti7出现登录验证的实现

    Springboot2.1.6集成activiti7出现登录验证的实现

    这篇文章主要介绍了Springboot2.1.6集成activiti7出现登录验证的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12

最新评论