Spring Boot开启虚拟线程ScopedValue上下文传递的使用方式

 更新时间:2026年02月04日 15:59:07   作者:Demon_Hao  
本文给大家介绍了ScopedValue在Java虚拟线程中的应用,解决了传统ThreadLocal在大量短生命周期线程场景下的问题,本文结合实例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧

1. 背景

在传统的 Java 应用中,ThreadLocal 常用于在同一线程中传递上下文信息(如请求ID、用户信息等)。
然而,随着 Java 虚拟线程(Virtual Thread) 的引入,线程数量可以非常大(成千上万),ThreadLocal 在这种场景下存在几个问题:

  • 内存泄漏风险:线程长期存在时,ThreadLocal 变量容易被残留引用占用。
  • 上下文传递复杂:虚拟线程切换可能导致 ThreadLocal 值不一致,尤其在使用异步或挂起操作时。

为了解决这个问题,Java 提供了 ScopedValue,用于在虚拟线程中安全、轻量地传递上下文。

2. ScopedValue 特点

  • 轻量级:与 ThreadLocal 不同,它不会在每个线程上创建额外的存储空间。
  • 线程安全:值是不可变的,只能在创建的作用域内访问。
  • 自动传递:在虚拟线程中创建作用域时,内部逻辑可以自动将上下文传递给挂起和恢复操作。
  • 适合虚拟线程:与 ThreadLocal 相比,ScopedValue 更适合大量短生命周期线程的场景。

3. 使用方式

1、全局开启使用虚拟线程(yaml配置)

spring:
  main:
    # 保证 JVM 在全是虚拟线程情况下不会提前退出
    keep-alive: true
  # 全局虚拟线程开关(推荐方式)
  threads:
    virtual:
      # 启用虚拟线程,覆盖 TaskExecutor、@Async、@Scheduled、Web Server
      enabled: true

2、虚拟线程上下文传递参数

import lombok.Builder;
/**
 * 虚拟线程上下文传递参数
 *
 * @param traceId  链路ID(分布式微服务传递追踪)
 * @param userId   用户ID
 * @param tenantId 租户ID
 */
@Builder
public record RequestContext(
    String traceId,
    String userId,
    String tenantId) {
}

3、ScopedValue工具类

import lombok.NoArgsConstructor;
/**
 * ScopedValue工具类
 */
@NoArgsConstructor
public final class ContextKeys {
    // 链路ID
    public static final String TRACE_ID = "traceId";
    /**
     * WEB请求上下文传递
     */
    public static final ScopedValue<RequestContext> REQUEST_CONTEXT = ScopedValue.newInstance();
}

4、获取上下文业务参数

import lombok.NoArgsConstructor;
import org.slf4j.MDC;
import java.util.Optional;
import java.util.concurrent.Callable;
/**
 * 获取上下文业务参数
 */
@NoArgsConstructor
public final class RequestContextHolder {
    /**
     * 获取完整上下文
     */
    public static Optional<RequestContext> getOptional() {
        return ContextKeys.REQUEST_CONTEXT.isBound() ? Optional.of(ContextKeys.REQUEST_CONTEXT.get()) : Optional.empty();
    }
    /**
     * 获取 traceId
     */
    public static String getTraceId() {
        return getOptional().map(RequestContext::traceId).orElse(null);
    }
    /**
     * 获取 userId
     */
    public static String getUserId() {
        return getOptional().map(RequestContext::userId).orElse(null);
    }
    /**
     * 获取 tenantId
     */
    public static String getTenantId() {
        return getOptional().map(RequestContext::tenantId).orElse(null);
    }
    /**
     * 绑定上下文并运行 Runnable
     */
    public static void with(RequestContext ctx, Runnable task) {
        ScopedValue.where(ContextKeys.REQUEST_CONTEXT, ctx).run(() -> {
            try {
                // MDC桥接
                MDC.put(ContextKeys.TRACE_ID, ctx.traceId());
                task.run();
            } finally {
                MDC.clear();
            }
        });
    }
    /**
     * 绑定上下文并运行 Callable
     */
    public static <T> T with(RequestContext ctx, Callable<T> task) throws Exception {
        return ScopedValue.where(ContextKeys.REQUEST_CONTEXT, ctx).call(() -> {
            try {
                // MDC桥接
                MDC.put(ContextKeys.TRACE_ID, ctx.traceId());
                return task.call();
            } finally {
                MDC.clear();
            }
        });
    }
}

5、HTTP请求上下文初始化

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jetbrains.annotations.NotNull;
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
/**
 * HTTP请求上下文初始化
 */
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ContextInitFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
        RequestContext ctx = buildContext(request);
        // ScopedValue 绑定上下文
        try {
            ScopedValue.where(ContextKeys.REQUEST_CONTEXT, ctx).run(() -> {
                try {
                    // MDC桥接
                    MDC.put(ContextKeys.TRACE_ID, ctx.traceId());
                    filterChain.doFilter(request, response);
                } catch (IOException | ServletException e) {
                    throw new RuntimeException(e);
                } finally {
                    MDC.clear();
                }
            });
        } catch (RuntimeException e) {
            // 拆包,保持Servlet语义
            if (e.getCause() instanceof IOException io) throw io;
            if (e.getCause() instanceof ServletException se) throw se;
            throw e;
        }
    }
    /**
     * 构建请求上下文
     */
    private RequestContext buildContext(HttpServletRequest request) {
        // 链路ID
        String traceId = Optional.ofNullable(request.getHeader("X-Trace-Id")).orElse(UUID.randomUUID().toString());
        // 用户ID
        String userId = request.getHeader("X-User-Id");
        // 租户ID
        String tenantId = request.getHeader("X-Tenant-Id");
        return RequestContext.builder().traceId(traceId).userId(userId).tenantId(tenantId).build();
    }
}

6、ScopedValue和StructuredTaskScope使用方式

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@RestController
public class DemoController {
    @GetMapping("/test")
    public String test() {
        // 获取参数
        String tenantId = RequestContextHolder.getTenantId();
        String userId = RequestContextHolder.getUserId();
        // 新虚拟线程执行
        RequestContext ctx = RequestContextHolder.getOptional().orElseThrow();
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
        executor.submit(() -> {
            RequestContextHolder.with(ctx, () -> {
                // 异步任务中依然可以获取 traceId / userId
                System.out.println("traceId=" + RequestContextHolder.getTraceId());
            });
        });
        return "tenantId=" + tenantId + ", userId=" + userId;
    }
    /**
     * StructuredTaskScope实现:同步写法 + 并发执行 + 自动失败传播
     * 非常类似WebFlux/Reactor的:Mono.zip(callA(), callB()).map(tuple -> combine(tuple.getT1(), tuple.getT2()));
     */
    @Transactional
    public void service() {
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            var a = scope.fork(this::taskA);
            var b = scope.fork(this::taskB);
            // 等待所有
            scope.join();
            // 有失败就抛
            scope.throwIfFailed();
            return combine(a.get(), b.get());
        }
    }
}

到此这篇关于Spring Boot开启虚拟线程ScopedValue上下文传递的文章就介绍到这了,更多相关Spring Boot开启虚拟线程内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring整合Mybatis框架方法剖析

    Spring整合Mybatis框架方法剖析

    这篇文章主要为大家介绍了Spring整合Mybatis框架方法剖析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-07-07
  • java获取百度网盘真实下载链接的方法

    java获取百度网盘真实下载链接的方法

    这篇文章主要介绍了java获取百度网盘真实下载链接的方法,涉及java针对URL操作及页面分析的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-07-07
  • ToStringBuilder类的一些心得

    ToStringBuilder类的一些心得

    ToStringBuilder类的一些心得,需要的朋友可以参考一下
    2013-02-02
  • 使用System.exit()来优雅地终止SpringBoot项目的代码示例

    使用System.exit()来优雅地终止SpringBoot项目的代码示例

    System.exit() 方法是 Java 中用于退出程序的方法,它接受一个整数参数,通常被用来指示程序的退出状态,本文给大家介绍了如何使用System.exit()来优雅地终止SpringBoot项目,需要的朋友可以参考下
    2024-08-08
  • Java Spring框架简介与Spring IOC详解

    Java Spring框架简介与Spring IOC详解

    Spring 框架是一个轻量级的解决方案,可以一站式地构建企业级应用。它是为了解决 企业应用开发的复杂性而创建的。Spring 使用基本的 JavaBean 来完成以前只可能由 EJB 完成的事情。IOC 是 Inversion of Control 的缩写,多数书籍翻译成控制反转
    2021-09-09
  • java为什么不建议用equals判断对象相等

    java为什么不建议用equals判断对象相等

    本文主要介绍了java为什么不建议用equals判断对象相等,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-03-03
  • 在Java中去除字符串末尾的换行符的常用方法小结

    在Java中去除字符串末尾的换行符的常用方法小结

    在日常开发中,字符串操作是非常常见的需求,其中去除字符串末尾的换行符(\n)是一个很有代表性的场景,本文将从 Java 的几种常用方法着手,全面解析如何去除字符串末尾的换行符,并结合代码示例和实际使用场景,帮助大家掌握这一技巧,需要的朋友可以参考下
    2024-12-12
  • 解决IDEA集成Docker插件后出现日志乱码的问题

    解决IDEA集成Docker插件后出现日志乱码的问题

    这篇文章主要介绍了解决IDEA集成Docker插件后出现日志乱码的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-11-11
  • SpringBoot+MyBatis进行XML中循环处理List参数的终极指南

    SpringBoot+MyBatis进行XML中循环处理List参数的终极指南

    这篇文章主要为大家详细介绍了SpringBoot整合MyBatis进行XML中循环处理List参数的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下
    2025-07-07
  • 使用ScheduledThreadPoolExecutor踩过最痛的坑

    使用ScheduledThreadPoolExecutor踩过最痛的坑

    这篇文章主要介绍了使用ScheduledThreadPoolExecutor踩过最痛的坑及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-08-08

最新评论