服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Java教程 - 基于TTL 解决线程池中 ThreadLocal 线程无法共享的问题

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

2024-04-08 15:17码上遇见你 Java教程

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

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


原文地址:https://mp.weixin.qq.com/s/nOAOspvGecjj2evX1Xtq_g

延伸 · 阅读

精彩推荐