Java线程池——ThreadLocal上下文污染问题

文章目录

前言

池化技术在Spring应用中比较常见,通常有一些数据库连接池自定义连接池Tomcat 请求线程池等等。池化技术极大的增加了线程的复用性,降低了线程的频繁创建和销毁的性能开销问题。

但不规范的线程池使用,会导致一些极大的生产问题。

下面以自定义线程池为例,重点说明一下ThreadLocal上下文污染问题以及处理方式。

什么是ThreadLocal上下文污染

说上下文污染问题,首先先说一下线程池的原理、使用和线程销毁等细节。

在Java语言中,线程池的创建申明,通常使用JUC中给定的API实现。可以看看往期我的博客:

JUC之线程池

JUC之线程池七大参数理解

线程池的核心通常分为核心线程队列非核心线程(最大减去核心数)拒绝策略等。

线程池在使用时,通常分为以下几个阶段:

  • 1、首次调用使用时,线程池分配核心线程进行处理。
  • 2、第二次调用使用时,线程池判断核心线程是否全部占用,若未全部占用,则继续分配核心线程处理,若已全部占用则将该线程处理置于队列中。
  • 3、后续调用使用时,监测到核心线程满,队列满时,则会构建空闲线程,用于逻辑处理。
  • 4、超出最大线程数和队列长度时,后续调用使用会直接进行拒绝策略

这里面有几个要点:

1、线程池初始化时,就会自动创建指定核心数的半激活线程置于池中。

2、非核心线程在使用后,不会立即进行清理,会根据线程池的算法机制判断空闲线程的销毁时机。

3、Java应用不销毁或者容器不销毁,创建的自定义线程池不会进行销毁操作。

这样就会引发一个问题:

如果在某个核心线程使用时,给这个线程的ThreadLocalMap绑定某些值,在使用完成后也不去clear。

在后续使用线程池时,就可能会拿到复用的线程,在不设置值的情况下就能取到之前业务操作设定的属性值。导致上下文污染,也称为 内存泄漏。

自定义线程池验证

定义 ThreadLocal 操作工具类

定义一个ThradLocal的操作类,提供设置值和获取值的方法:

java 复制代码
import cn.hutool.core.map.MapUtil;

import java.util.Map;

public class SecurityContextHolder {
    // 这是唯一能作为 ThreadLocalMap key 的东西
    private static final ThreadLocal<Map<String, Object>> CONTEXT = new ThreadLocal<>();

    public static void set(String key, Object value) {
        Map<String, Object> map = CONTEXT.get();
        if (map == null) {
            map = MapUtil.newHashMap();
            CONTEXT.set(map);
        }
        map.put(key, value);
    }
    
	public static void setAll(Map<String,Object> mapData) {
		if (mapData == null || mapData.isEmpty()) return;
	    Map<String, Object> map = CONTEXT.get();
	    if (map == null) {
	        map = MapUtil.newHashMap();
	        CONTEXT.set(map);
	    }
	    map.putAll(mapData);
	}
    
    /**
     * Get all key-value pairs stored in the current thread's ThreadLocal context.
     *
     * @return a Map containing all context data, or null if no context is set
     */
    public static Map<String, Object> getAll() {
        return CONTEXT.get();
    }

    public static Object get(String key) {
        Map<String, Object> map = CONTEXT.get();
        return map == null ? null : map.get(key);
    }

    public static void clear() {
        CONTEXT.remove();
    }
}

创建自定义线程池

定义线程池,设定关键参数:

java 复制代码
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Configuration
public class AsyncTaskTheadPoolConfig {

    /**
     * 核心线程数
     * 默认的核心线程数为1
     */
    private static final int CORE_POOL_SIZE = 5;
    /**
     * 最大线程数
     * 默认的最大线程数是Integer.MAX_VALUE 即2<sup>31</sup>-1
     */
    private static final int MAX_POOL_SIZE = 1500;
    /**
     * 缓冲队列数
     * 默认的缓冲队列数是Integer.MAX_VALUE 即2<sup>31</sup>-1
     */
    private static final int QUEUE_CAPACITY = 4000;

    /**
     * 允许线程空闲时间
     * 默认的线程空闲时间为60秒
     */
    private static final int KEEP_ALIVE_SECONDS = 60;

    /**
     * 线程池前缀名
     */
    private static final String THREAD_NAME_PREFIX = "Task_Service_Async_";
    public static ThreadPoolTaskExecutor executor;
    public static ThreadPoolTaskScheduler taskScheduler;
    /**
     * allowCoreThreadTimeOut为true则线程池数量最后销毁到0个
     * allowCoreThreadTimeOut为false
     * 销毁机制:超过核心线程数时,而且(超过最大值或者timeout过),就会销毁。
     * 默认是false
     */
    private boolean allowCoreThreadTimeOut = false;

    @Bean
    public ThreadPoolTaskScheduler threadPoolTaskScheduler(ThreadPoolTaskExecutor taskExecutor) {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler = threadPoolTaskScheduler;
        threadPoolTaskScheduler.setThreadFactory(taskExecutor);
        // 指定线程处理 环绕 方法,spring 3.0 提供
        //threadPoolTaskScheduler.setTaskDecorator(new MyTaskDecorator());
        return threadPoolTaskScheduler;
    }

    @Bean
    public ExecutorService getExecutorService() {
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNameFormat(THREAD_NAME_PREFIX + "%d").build();
        return new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE,
                KEEP_ALIVE_SECONDS, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(QUEUE_CAPACITY), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
    }

    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(CORE_POOL_SIZE);
        taskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
        taskExecutor.setQueueCapacity(QUEUE_CAPACITY);
        taskExecutor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
        taskExecutor.setThreadNamePrefix(THREAD_NAME_PREFIX);
        taskExecutor.setAllowCoreThreadTimeOut(allowCoreThreadTimeOut);
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //taskExecutor.setTaskDecorator(new MyTaskDecorator());
        //线程池初始化
        taskExecutor.initialize();
        executor = taskExecutor;
        return taskExecutor;
    }
}

案例验证

如下所示,以下逻辑进行验证:

java 复制代码
import com.xj.AiMcpToolApplication;
import com.xj.config.AsyncTaskTheadPoolConfig;
import com.xj.constant.SecurityContextHolder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.TimeUnit;

@SpringBootTest(classes = AiMcpToolApplication.class)
public class SpringBootTest2 {

    @Autowired
    private AsyncTaskTheadPoolConfig asyncTaskTheadPoolConfig;

    @Test
    public void test1() throws InterruptedException {
        System.out.println("主线程打印");
        asyncTaskTheadPoolConfig.executor.submit(()->{
            System.out.println(Thread.currentThread().getName() +" 子线程 set ThreadLocalMap 值");
            // 设置 ThradLocalMap 值
            SecurityContextHolder.set("name","xj");
        });
        
        // 循环操作 次数最好高于设定核心线程数
        for (int i = 0; i < 10; i++) {
            TimeUnit.SECONDS.sleep(2);
            asyncTaskTheadPoolConfig.executor.submit(()->{
                System.out.println(Thread.currentThread().getName() +" 子线程 get ThreadLocalMap 值");
                System.out.println(SecurityContextHolder.get("name"));
            });
        }

    }
}

运行后控制台的效果如下:

处理方式

通常来说,从线程池中获取某个线程使用后,及时地将ThreadLocalMap进行清理即可。

低版本中,可以这样使用:

java 复制代码
asyncTaskTheadPoolConfig.executor.submit(()->{
	try {
		SecurityContextHolder.set("name","xj");
		doSomething();
	} finally {
		SecurityContextHolder.clear();
	}
});

Spring 4.3+中,对应线程池调度器ThreadPoolTaskExecutor有一个特殊的处理方式org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor#setTaskDecorator(TaskDecorator taskDecorator)

看源码可以得知,TaskDecorator是一个接口,也就是说可以自定义某个类实现这个接口重写其中的核心逻辑就能使用。

自定义环绕处理类

java 复制代码
import com.crfsdi.amt.constant.SecurityContextHolder;
import org.springframework.core.task.TaskDecorator;

import java.util.Map;

public class MyTaskDecorator implements TaskDecorator {
    @Override
    public Runnable decorate(Runnable runnable) {
        Map<String, Object> allParentMap = SecurityContextHolder.getAll();
        return () -> {
            try {
                // 子线程继承父线程的SecurityContext
                SecurityContextHolder.setAll(allParentMap);
                runnable.run(); // 执行异步任务
            } finally { // 线程池任务执行结束则清理上下文
                // 子线程执行结束后清理SecurityContextHolder
                SecurityContextHolder.clear();
                System.out.println("finally 清理 ThreadLocalMap 数据");
            }
        };
    }
}

线程池配置类中绑定线程执行器

java 复制代码
@Bean
public ThreadPoolTaskScheduler threadPoolTaskScheduler(ThreadPoolTaskExecutor taskExecutor) {
    ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
    taskScheduler = threadPoolTaskScheduler;
    threadPoolTaskScheduler.setThreadFactory(taskExecutor);
    // spring 6.2+ 版本才有 绑定自定义处理类
    threadPoolTaskScheduler.setTaskDecorator(new MyTaskDecorator());
    return threadPoolTaskScheduler;
}

@Bean
public ThreadPoolTaskExecutor taskExecutor() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setCorePoolSize(CORE_POOL_SIZE);
    taskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
    taskExecutor.setQueueCapacity(QUEUE_CAPACITY);
    taskExecutor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
    taskExecutor.setThreadNamePrefix(THREAD_NAME_PREFIX);
    taskExecutor.setAllowCoreThreadTimeOut(allowCoreThreadTimeOut);
    taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

    // 绑定自定义执行器
    taskExecutor.setTaskDecorator(new MyTaskDecorator());
    //线程池初始化
    taskExecutor.initialize();
    executor = taskExecutor;
    return taskExecutor;
}

再次调用上面的测试类。控制台打印信息如下:

总结

使用完线程池中的线程后,需要及时清理线程的ThreadLocalMap值!

相关推荐
武子康10 小时前
Java-09 深入浅出 MyBatis 注解开发详解:从 CRUD 到复杂关系映射
java·后端·spring
Amctwd10 小时前
【后端】多个后端系统,如何共用一套登录状态?单点登录详解
java
用户2986985301410 小时前
Java 进阶:在 Word 文档中动态增删页面
java·后端
likerhood10 小时前
Java 集合框架入门:List、Set、Queue 与 Map
java·开发语言·list
Java 码思客10 小时前
【Spring AI实战】第2章 大模型基础调用:同步/异步/流式输出
java·人工智能·spring·ai
郝学胜-神的一滴10 小时前
系统设计 013:高并发系统缓存:从原理到实践全解析
java·开发语言·python·缓存·系统架构·php·软件构建
欧米欧10 小时前
C++进阶之AVL树
java·服务器·c++
我是一只码蚁10 小时前
记一次苍穹外卖项目 Maven 编译报错的排查与解决全过程
java·经验分享·笔记·后端·架构·maven
i220818 Faiz Ul10 小时前
理财系统|基于java+vue的家庭理财系统小程序(源码+数据库+文档)
java·vue.js·spring boot·小程序·论文·毕设·理财系统