框架建设实战8——线程池处理组件

31 阅读3分钟

 在业务开发中,如果需要异步处理,除了使用mq的方式外,还有一种就是借助线程池的方式。

为了统一大家使用线程池的使用,我们可以通过组件的方式,统一大家的使用习惯。比如线程池定义、线程池上下文处理等。

比如,场景的spring自带的@Async注解,就是基于线程池的方式处理的。

对此,我们需要对@Async的使用方式进行统一,以免造成错用。

1.线程池可配置

@Value("${frame.threadpool.corePoolSize:10}")
    private int corePoolSize;
    @Value("${frame.threadpool.maxPoolSize:20}")
    private int maxPoolSize;
    @Value("${frame.threadpool.queueCapacity:50}")
    private int queueCapacity;
    @Value("${frame.threadpool.idletime:300}")
    private int idleTime;

其中配置中心,基于apollo或者nacos即可。

2.bean定义

@Bean
    public Executor taskExecutor() {
        log.info("创建框架定义异步线程池");
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //mdc传递
        executor.setTaskDecorator(new MyTaskDecorator());
        //设置核心线程数
        executor.setCorePoolSize(corePoolSize);
        //线程池维护线程的最大数量,只有在缓冲队列满了以后才会申请超过核心线程数的线程
        executor.setMaxPoolSize(maxPoolSize);
        //缓存队列
        executor.setQueueCapacity(queueCapacity);
        //允许的空闲时间,当超过了核心线程数之外的线程在空闲时间到达之后会被销毁
        executor.setKeepAliveSeconds(idleTime);
        executor.setThreadNamePrefix("async-task-");
        // 线程拒绝策略,由调用线程处理该任务
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

3.自定义任务装饰器

class MyTaskDecorator implements TaskDecorator{
        @Override
        public Runnable decorate(Runnable runnable) {
            try {
                RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
                Map<String, String> contextMap = MDC.getCopyOfContextMap();
                return () -> {
                    try {
                        // Restore the web thread MDC context
                        if(contextMap != null) {
                            MDC.setContextMap(contextMap);
                        }
                        RequestContextHolder.setRequestAttributes(attributes);
                        runnable.run();
                    } finally {
                        MDC.clear();
                        RequestContextHolder.resetRequestAttributes();
                    }
                };
            } catch (IllegalStateException e) {
                return runnable;
            }
        }
    }

扩展自己的任务装饰器,可以满足MDC上下文透传、request请求信息在线程间透传等。

另外,基于以上的方式,我们可以同样对线程池的定义,进行一层定制的wrapper包装。

/**
 * ThreadPoolExecutor包装器
 */
public  class ThreadPoolExecutorWrapper extends ThreadPoolExecutor {
    public ThreadPoolExecutorWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                     BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }
    .......

    @Override
    public void execute(Runnable task) {
        super.execute(ThreadMdcUtil.wrap(task));
    }
}

其中,ThreadMdcUtil实现的方式跟上述自定义任务装饰器类似,在此不予赘述。 


进一步思考:

在上述代码中,我们看到定制的任务装饰器中加入了对MDC的处理,这个其实是为了日志处理服务的。我们看其定义:

Mapped Diagnostic Context(MDC)是一种在多线程环境中用于增强日志记录的功能,它允许开发人员将与当前线程相关的特定诊断信息与日志条目关联起来。MDC的核心实现通常依赖于java.lang.ThreadLocal类,为每个线程提供独立的存储空间,实现线程安全的数据隔离。

那我们基于MDC 处理 就可以了吗?不然。由于本文的重点是线程池处理。请读者自行了解

MDC/TheadLocal/InheritableThreadLocal的关系。

如果我们需要满足线程池下的父子上下文变量传递,应该怎么办呢?

阿里给出了一个较好的解决方案:TransmittableThreadLocal。

本文只是提供一个解决思路。请大家基于此思路优化我们的线程池组件吧。

请关注我的专栏,后续会有更详细的介绍。

参考:

juejin.cn/post/710679…

www.51cto.com/article/710…

TransmittableThreadLocal(TTL)实现线程变量传递的原理分析_ttlthreadlocal-CSDN博客