写点什么

✅基于 TTL 解决线程池中 ThreadLocal 线程无法共享的问题

作者:派大星
  • 2024-04-09
    辽宁
  • 本文字数:3216 字

    阅读完需:约 11 分钟

在 Java 的并发编程领域中,ThreadLocal 被广泛运用来解决线程安全困境,它巧妙地为每个线程提供独立的变量副本,有效规避了线程间数据共享的问题。

不过,在使用线程池时,传递线程局部变量在父子线程之间并非易事。这是因为 ThreadLocal 的设计初衷仅在于线程内的数据隔离,无法支持跨线程间的数据传递。

背景

在基于 Java 的应用开发领域,尤其是在利用 Spring 框架、异步处理和微服务架构构建系统时,常常需要在不同线程或服务之间传递用户会话、数据库事务或其他上下文信息。

举例来说,在处理用户请求的 Web 服务中,记录日志是必不可少的一环。这些日志需包含请求的独特标识(如请求 ID),这个 ID 在请求进入服务时生成,并会贯穿整个处理流程,包括可能并发执行的多个子任务或被分配到线程池中不同线程上执行。(在分布式场景中通常会称之为 traceId)

在这种情况下,使用 ThreadLocal 来存储请求 ID 会带来问题:并发执行的子任务无法访问父线程 ThreadLocal 中存储的请求 ID,而且在使用线程池时,线程的重用可能导致请求 ID 被错误地共享或丢失。

伪代码:

import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;
public class ThreadLocalExample {        private static ThreadLocal<String> requestId = new ThreadLocal<>();
    public static void main(String[] args) {        requestId.set("12345"); // 设置请求ID
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.submit(() -> {            System.out.println("Child task running in a separate thread: " + requestId.get());        });
        executor.shutdown();    }}

复制代码

在这个示例中,父线程设置了请求 ID 为"12345",但是当子任务在另一个线程中执行时,无法访问到父线程中的 ThreadLocal 变量 requestId,因此子任务无法获取到请求 ID,可能会输出 null 或者""。

伪代码:

import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;
public class ThreadLocalThreadPoolExample {        private static ThreadLocal<String> requestId = new ThreadLocal<>();
    public static void main(String[] args) {        requestId.set("12345"); // 设置请求ID
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.submit(() -> {            System.out.println("Child task running in a thread pool: " + requestId.get());        });
        // 另一个任务复用线程        executor.submit(() -> {            System.out.println("Another child task running in the same thread: " + requestId.get());        });
        executor.shutdown();    }}

复制代码

在这个示例中,如果线程池中的两个任务在同一个线程中执行,且没有正确处理 ThreadLocal 变量,可能会导致第二个任务获取到了第一个任务的请求 ID,导致请求 ID 的错误共享。

技术选型

为了应对这一难题,可以采用 TransmittableThreadLocal(TTL)这一阿里巴巴开源工具库,专为解决在使用线程池等会重用线程的情况下,ThreadLocal 无法正确管理线程上下文的问题而设计。

GitHub 开源地址:https://github.com/alibaba/transmittable-thread-local

TransmittableThreadLocal 基于 ThreadLocal 进行扩展,提供了跨线程传递数据的能力,确保父线程传递值给子线程,并支持线程池等场景下的线程数据隔离。

此外,还有 JDK 自带的 InheritableThreadLocal,用于主子线程间参数传递。然而,这种方式存在一个限制:必须在主线程手动创建子线程才可使用,而在线程池中则难以实现此种传递机制。

具体实现

依赖引入

首先,需在项目中引入 TransmittableThreadLocal 的依赖。若为 Maven 项目,可添加以下依赖:

<dependency>  <groupId>com.alibaba</groupId>  <artifactId>transmittable-thread-local</artifactId>  <version><!-- 使用最新版本 --></version> </dependency>
复制代码

使用 TransmittableThreadLocal 存储请求 ID

public class RequestContext {    // 使用TransmittableThreadLocal来存储请求ID    private static final ThreadLocal<String> requestIdTL = new TransmittableThreadLocal<>();
    public static void setRequestId(String requestId) {        requestIdTL.set(requestId);    }
    public static String getRequestId() {        return requestIdTL.get();    }
    public static void clear() {        requestIdTL.remove();    }}
复制代码
创建一个线程池,并使用 TTL 提供的工具类确保线程池兼容 TransmittableThreadLocal
import com.alibaba.ttl.threadpool.TtlExecutors;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;
public class ThreadPoolUtil {    private static final ExecutorService pool = Executors.newFixedThreadPool(10);
    // 使用TtlExecutors工具类包装原始的线程池,使其兼容TransmittableThreadLocal    public static final ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(pool);
    public static ExecutorService getExecutorService() {        return ttlExecutorService;    }}

复制代码

TtlExecutors 是 TransmittableThreadLocal(TTL)库中的一款实用工具类,其机制在于对 Java 标准库中的 ExecutorService、ScheduledExecutorService 等线程池接口的实例进行包装。

通过这种封装,确保在使用线程池时,能够正确地传递 TransmittableThreadLocal 中存储的上下文数据,即使任务在不同线程中执行。这对于解决在使用线程池时 ThreadLocal 变量值传递的问题至关重要。

执行并行任务,并在任务中使用 RequestContext 来访问请求 ID
import java.util.stream.IntStream;
public class Application {    public static void main(String[] args) {        // 模拟Web应用中为每个请求设置唯一的请求ID        String requestId = "REQ-" + System.nanoTime();        RequestContext.setRequestId(requestId);
        try {            ExecutorService executorService = ThreadPoolUtil.getExecutorService();
            IntStream.range(0, 5).forEach(i ->                 executorService.submit(() -> {                    // 在子线程中获取并打印请求ID                    System.out.println("Task " + i + " running in thread " + Thread.currentThread().getName() + " with Request ID: " + RequestContext.getRequestId());                })            );        } finally {            // 清理资源            RequestContext.clear();            ThreadPoolUtil.getExecutorService().shutdown();        }    }}

复制代码

如有问题,欢迎加微信交流:w714771310,备注- 技术交流  。或微信搜索【码上遇见你】。

免费的Chat GPT可微信搜索【AI贝塔】进行体验,无限使用。

好了,本章节到此告一段落。希望对你有所帮助,祝学习顺利。

发布于: 刚刚阅读数: 5
用户头像

派大星

关注

微信搜索【码上遇见你】,获取更多精彩内容 2021-12-13 加入

微信搜索【码上遇见你】,获取更多精彩内容

评论

发布
暂无评论
✅基于TTL 解决线程池中 ThreadLocal 线程无法共享的问题_ThreadLocal_派大星_InfoQ写作社区