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

原创 派大星 码上遇见你 2024-04-08 10:06 辽宁

码上遇见你

身处外企,是一名技术工作者。作为InfoQ、阿里云等技术平台签约作者,致力于打造您专属的编程个人笔记(无广告)。

216篇原创内容

公众号

在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();
        }
    }
}

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
线程池线程对象是可以被多个任务共享的,如果线程对象需要保存任务相关的数据,使用 ThreadLocal 可以保证线程安全。 下面是一个使用 ThreadLocalJava 代码示例: ```java public class MyTask implements Runnable { private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); private int taskId; public MyTask(int id) { this.taskId = id; } @Override public void run() { threadLocal.set(taskId); System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName() + ", task id stored in thread local: " + threadLocal.get()); // do some task threadLocal.remove(); } } public class ThreadPoolDemo { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(3); for (int i = 0; i < 5; i++) { executorService.submit(new MyTask(i)); } executorService.shutdown(); } } ``` 在这个示例,我们创建了一个 MyTask 类,它实现了 Runnable 接口。在 MyTask 类,我们使用 ThreadLocal 存储了任务的 ID,以保证不同的任务之间不会相互影响。在 run() 方法,我们首先将任务的 ID 存储到 ThreadLocal ,然后执行任务,并在任务执行完毕后从 ThreadLocal 移除任务的 ID。 然后我们创建了一个固定大小为 3 的线程池,并通过 submit() 方法将 5 个任务提交到线程池执行。由于线程池线程对象是可以被多个任务共享的,因此这 5 个任务会在 3 个线程共享执行。在执行每个任务时,我们可以看到任务 ID 在不同的线程被正确地存储和读取,以保证了线程安全。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值