前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于TTL 解决线程池中 ThreadLocal 线程无法共享的问题

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

作者头像
@派大星
发布2024-04-15 13:54:29
1120
发布2024-04-15 13:54:29
举报
文章被收录于专栏:码上遇见你码上遇见你

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

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

背景

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

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

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

伪代码:

代码语言:javascript
复制
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或者""。

伪代码:

代码语言:javascript
复制
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项目,可添加以下依赖:

代码语言:javascript
复制
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>transmittable-thread-local</artifactId>
  <version><!-- 使用最新版本 --></version> 
</dependency>
使用TransmittableThreadLocal存储请求ID
代码语言:javascript
复制
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
代码语言:javascript
复制
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
代码语言:javascript
复制
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();
        }
    }
}

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-04-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码上遇见你 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 技术选型
  • 具体实现
    • 依赖引入
      • 使用TransmittableThreadLocal存储请求ID
        • 创建一个线程池,并使用TTL提供的工具类确保线程池兼容TransmittableThreadLocal
        • 执行并行任务,并在任务中使用RequestContext来访问请求ID
    相关产品与服务
    对象存储
    对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档