SpringBoot ThreadLocal 父子线程传值的几种实现方式

前言

在日常开发中,我们经常会遇到需要在父子线程之间传递数据的场景。比如用户身份信息、请求ID、链路追踪ID等。

ThreadLocal作为Java中重要的线程本地变量机制,为我们提供了在单个线程内存储数据的便利。但是,当涉及到父子线程之间的数据传递时,ThreadLocal默认的行为并不能满足我们的需求。

本文将介绍在SpringBoot应用中实现ThreadLocal父子线程传值的几种方式。

ThreadLocal 基础回顾

首先,让我们简单回顾一下ThreadLocal的基本原理:

java 复制代码
public class ThreadLocal<T> {
    // 每个线程都有自己独立的ThreadLocalMap
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
}

ThreadLocal的核心思想是为每个线程维护一个独立的ThreadLocalMap,从而实现线程隔离。

问题的提出

让我们通过一个简单的例子来看看ThreadLocal在父子线程中的默认行为

java 复制代码
public class ThreadLocalDemo {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        threadLocal.set("主线程的值");
        System.out.println("主线程获取: " + threadLocal.get()); // 输出: 主线程的值

        Thread childThread = new Thread(() -> {
            System.out.println("子线程获取: " + threadLocal.get()); // 输出: null
        });
        childThread.start();
    }
}

从上面的例子可以看出,子线程无法访问父线程中设置的ThreadLocal值。这是因为每个线程都有自己独立的ThreadLocalMap。

解决方案一:InheritableThreadLocal

Java为我们提供了InheritableThreadLocal来解决父子线程传值的问题

java 复制代码
public class InheritableThreadLocalDemo {
    private static InheritableThreadLocal<String> inheritableThreadLocal =
        new InheritableThreadLocal<>();

    public static void main(String[] args) {
        inheritableThreadLocal.set("主线程的值");
        System.out.println("主线程获取: " + inheritableThreadLocal.get());

        Thread childThread = new Thread(() -> {
            System.out.println("子线程获取: " + inheritableThreadLocal.get()); // 输出: 主线程的值
        });
        childThread.start();
    }
}

InheritableThreadLocal 原理分析

InheritableThreadLocal的实现在Thread类中

java 复制代码
public class Thread implements Runnable {
    // 父线程的inheritableThreadLocals会在创建子线程时复制
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        // ...
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        // ...
    }
}

InheritableThreadLocal 的局限性

  • 1. 时机限制:只在创建线程时进行值传递,后续修改不会传递到已创建的子线程
  • 2. 线程池问题:在线程池中使用时,由于线程会被复用,可能导致数据混乱

解决方案二:使用TransmittableThreadLocal

阿里巴巴开源的TransmittableThreadLocal(TTL)是解决线程池场景下父子线程传值问题的优秀方案:

添加依赖

xml 复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.5</version>
</dependency>

基本使用

java 复制代码
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.threadpool.TtlExecutors;

public class TtlDemo {
    private static TransmittableThreadLocal<String> ttl = new TransmittableThreadLocal<>();

    public static void main(String[] args) {
        ttl.set("主线程的值");
        System.out.println("主线程获取: " + ttl.get());

        // 使用TTL装饰的线程池
        ExecutorService executor = TtlExecutors.getTtlExecutorService(
            Executors.newFixedThreadPool(2)
        );

        executor.submit(() -> {
            System.out.println("线程池任务1获取: " + ttl.get()); // 输出: 主线程的值
        });

        // 修改值后提交新任务
        ttl.set("更新后的值");
        executor.submit(() -> {
            System.out.println("线程池任务2获取: " + ttl.get()); // 输出: 更新后的值
        });
    }
}

TTL 与Spring Boot的集成

在Spring Boot应用中,我们可以通过以下方式集成TTL

1. 配置TTL异步执行器
java 复制代码
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("Async-");

        // 使用TTL装饰
        return TtlExecutors.getTtlExecutor(executor);
    }
}
2. 使用TTL的请求拦截器
java 复制代码
@Component
public class TtlRequestInterceptor implements HandlerInterceptor {

    private static final TransmittableThreadLocal<String> requestId =
        new TransmittableThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                           Object handler) {
        String id = UUID.randomUUID().toString();
        requestId.set(id);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                              Object handler, Exception ex) {
        requestId.remove(); // 清理
    }

    public static String getRequestId() {
        return requestId.get();
    }
}

解决方案三:自定义TaskDecorator

Spring提供了TaskDecorator接口,允许我们对Runnable任务进行装饰

java 复制代码
@Configuration
@EnableAsync
public class CustomAsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("CustomAsync-");

        // 设置自定义装饰器
        executor.setTaskDecorator(new ContextCopyingDecorator());
        executor.initialize();

        return executor;
    }

    private static class ContextCopyingDecorator implements TaskDecorator {
        @Override
        public Runnable decorate(Runnable runnable) {
            // 获取父线程的ThreadLocal上下文
            Map<String, Object> context = getContextFromCurrentThread();
            return () -> {
                try {
                    // 在子线程中复制上下文
                    setContextToCurrentThread(context);
                    runnable.run();
                } finally {
                    clearCurrentThreadContext();
                }
            };
        }

        private Map<String, Object> getContextFromCurrentThread() {
            Map<String, Object> context = new HashMap<>();
            // 收集当前线程的ThreadLocal值
            // 例如:从自定义的ThreadLocal中获取
            if (UserContext.getUser() != null) {
                context.put("userInfo", UserContext.getUser());
            }
            if (RequestContextHolder.getRequestAttributes() != null) {
                context.put("requestAttributes", RequestContextHolder.getRequestAttributes());
            }
            return context;
        }

        private void setContextToCurrentThread(Map<String, Object> context) {
            // 在子线程中设置上下文
            if (context.containsKey("userInfo")) {
                UserContext.setUser((UserContext.UserInfo) context.get("userInfo"));
            }
            if (context.containsKey("requestAttributes")) {
                RequestContextHolder.setRequestAttributes((RequestAttributes) context.get("requestAttributes"));
            }
        }

        private void clearCurrentThreadContext() {
            // 清理上下文,防止内存泄漏
            UserContext.clear();
            RequestContextHolder.resetRequestAttributes();
        }
    }
}

解决方案四:使用Spring的RequestContextHolder

在Spring Web应用中,我们可以利用RequestContextHolder来传递请求上下文

java 复制代码
@Service
public class ContextService {

    @Async
    public CompletableFuture<String> asyncMethod() {
        // 获取父线程的请求上下文
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();

        return CompletableFuture.supplyAsync(() -> {
            // 在异步线程中设置上下文
            RequestContextHolder.setRequestAttributes(attributes);

            try {
                // 执行业务逻辑
                String result = doSomeWork();
                return result;
            } finally {
                RequestContextHolder.resetRequestAttributes();
            }
        });
    }

    private String doSomeWork() {
        // 获取请求相关的信息
        HttpServletRequest request =
            ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        return "处理完成: " + request.getRequestURI();
    }
}

方案选择建议

基于上述性能对比和适用场景,我们可以给出以下选择建议:

  • 1. 简单的父子线程传值:使用InheritableThreadLocal
  • 2. 线程池场景:推荐使用TransmittableThreadLocal
  • 3. Spring异步任务:使用TaskDecorator
  • 4. Web应用请求上下文:使用RequestContextHolder

最佳实践

1. 内存泄漏预防

无论使用哪种方案,都要注意及时清理ThreadLocal:

java 复制代码
public class SafeThreadLocalUsage {
    private static ThreadLocal<Object> threadLocal = new ThreadLocal<>();

    public void doWork() {
        try {
            threadLocal.set(someValue);
            // 业务逻辑
        } finally {
            threadLocal.remove(); // 防止内存泄漏
        }
    }
}

2. 上下文封装

建议封装一个统一的上下文管理器:

java 复制代码
public class UserContext {
    private static final ThreadLocal<UserInfo> USER_CONTEXT =
        new TransmittableThreadLocal<>();

    public static void setUser(UserInfo user) {
        USER_CONTEXT.set(user);
    }

    public static UserInfo getUser() {
        return USER_CONTEXT.get();
    }

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

    public static class UserInfo {
        private String userId;
        private String userName;
        private String requestId;
        // getters and setters
    }
}

3. 拦截器统一管理

java 复制代码
@Component
public class UserContextInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                           Object handler) {
        UserInfo userInfo = buildUserInfo(request);
        UserContext.setUser(userInfo);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                              Object handler, Exception ex) {
        UserContext.clear();
    }

    private UserInfo buildUserInfo(HttpServletRequest request) {
        UserInfo userInfo = new UserInfo();
        userInfo.setUserId(request.getHeader("X-User-Id"));
        userInfo.setRequestId(UUID.randomUUID().toString());
        return userInfo;
    }
}

总结

本文介绍了四种在SpringBoot中实现ThreadLocal父子线程传值的方案:

  • 1. InheritableThreadLocal:最简单的原生解决方案,适用于基础场景
  • 2. TransmittableThreadLocal:功能强大的第三方解决方案,特别适合线程池场景
  • 3. TaskDecorator:Spring提供的优雅解决方案,集成度高
  • 4. RequestContextHolder:Spring Web应用的内置方案

在实际项目中,我们应该根据具体的业务场景和技术栈选择合适的方案。

同时,要注意内存管理和上下文清理,确保系统的稳定性和性能。

相关推荐
Victor3561 小时前
Redis(162)如何使用Redis实现消息队列?
后端
Victor3561 小时前
Redis(163)如何使用Redis实现计数器?
后端
KD1 小时前
数据密集型应用系统设计——面试总结版
后端
i***132410 小时前
Spring BOOT 启动参数
java·spring boot·后端
IT_Octopus10 小时前
(旧)Spring Securit 实现JWT token认证(多平台登录&部分鉴权)
java·后端·spring
kk哥889910 小时前
Spring详解
java·后端·spring
S***267510 小时前
Spring Cloud Gateway 整合Spring Security
java·后端·spring
码事漫谈10 小时前
C++单元测试框架选型与实战速查手册
后端
OneLIMS10 小时前
Windows Server 2022 + IIS + ASP.NET Core 完整可上传大文件的 报错的问题
windows·后端·asp.net