Spring Boot開(kāi)啟虛擬線程ScopedValue上下文傳遞的使用方式
1. 背景
在傳統(tǒng)的 Java 應(yīng)用中,ThreadLocal 常用于在同一線程中傳遞上下文信息(如請(qǐng)求ID、用戶信息等)。
然而,隨著 Java 虛擬線程(Virtual Thread) 的引入,線程數(shù)量可以非常大(成千上萬(wàn)),ThreadLocal 在這種場(chǎng)景下存在幾個(gè)問(wèn)題:
- 內(nèi)存泄漏風(fēng)險(xiǎn):線程長(zhǎng)期存在時(shí),ThreadLocal 變量容易被殘留引用占用。
- 上下文傳遞復(fù)雜:虛擬線程切換可能導(dǎo)致 ThreadLocal 值不一致,尤其在使用異步或掛起操作時(shí)。
為了解決這個(gè)問(wèn)題,Java 提供了 ScopedValue,用于在虛擬線程中安全、輕量地傳遞上下文。
2. ScopedValue 特點(diǎn)
- 輕量級(jí):與 ThreadLocal 不同,它不會(huì)在每個(gè)線程上創(chuàng)建額外的存儲(chǔ)空間。
- 線程安全:值是不可變的,只能在創(chuàng)建的作用域內(nèi)訪問(wèn)。
- 自動(dòng)傳遞:在虛擬線程中創(chuàng)建作用域時(shí),內(nèi)部邏輯可以自動(dòng)將上下文傳遞給掛起和恢復(fù)操作。
- 適合虛擬線程:與 ThreadLocal 相比,ScopedValue 更適合大量短生命周期線程的場(chǎng)景。
3. 使用方式
1、全局開(kāi)啟使用虛擬線程(yaml配置)
spring:
main:
# 保證 JVM 在全是虛擬線程情況下不會(huì)提前退出
keep-alive: true
# 全局虛擬線程開(kāi)關(guān)(推薦方式)
threads:
virtual:
# 啟用虛擬線程,覆蓋 TaskExecutor、@Async、@Scheduled、Web Server
enabled: true2、虛擬線程上下文傳遞參數(shù)
import lombok.Builder;
/**
* 虛擬線程上下文傳遞參數(shù)
*
* @param traceId 鏈路ID(分布式微服務(wù)傳遞追蹤)
* @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請(qǐng)求上下文傳遞
*/
public static final ScopedValue<RequestContext> REQUEST_CONTEXT = ScopedValue.newInstance();
}4、獲取上下文業(yè)務(wù)參數(shù)
import lombok.NoArgsConstructor;
import org.slf4j.MDC;
import java.util.Optional;
import java.util.concurrent.Callable;
/**
* 獲取上下文業(yè)務(wù)參數(shù)
*/
@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);
}
/**
* 綁定上下文并運(yùn)行 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();
}
});
}
/**
* 綁定上下文并運(yùn)行 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請(qǐng)求上下文初始化
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請(qǐng)求上下文初始化
*/
@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語(yǔ)義
if (e.getCause() instanceof IOException io) throw io;
if (e.getCause() instanceof ServletException se) throw se;
throw e;
}
}
/**
* 構(gòu)建請(qǐng)求上下文
*/
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() {
// 獲取參數(shù)
String tenantId = RequestContextHolder.getTenantId();
String userId = RequestContextHolder.getUserId();
// 新虛擬線程執(zhí)行
RequestContext ctx = RequestContextHolder.getOptional().orElseThrow();
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(() -> {
RequestContextHolder.with(ctx, () -> {
// 異步任務(wù)中依然可以獲取 traceId / userId
System.out.println("traceId=" + RequestContextHolder.getTraceId());
});
});
return "tenantId=" + tenantId + ", userId=" + userId;
}
/**
* StructuredTaskScope實(shí)現(xiàn):同步寫法 + 并發(fā)執(zhí)行 + 自動(dòng)失敗傳播
* 非常類似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());
}
}
}到此這篇關(guān)于Spring Boot開(kāi)啟虛擬線程ScopedValue上下文傳遞的文章就介紹到這了,更多相關(guān)Spring Boot開(kāi)啟虛擬線程內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
利用POI讀取word、Excel文件的最佳實(shí)踐教程
Apache POI 是用Java編寫的免費(fèi)開(kāi)源的跨平臺(tái)的 Java API,Apache POI提供API給Java程式對(duì)Microsoft Office格式檔案讀和寫的功能。 下面這篇文章主要給大家介紹了關(guān)于利用POI讀取word、Excel文件的最佳實(shí)踐的相關(guān)資料,需要的朋友可以參考下。2017-11-11
spring容器啟動(dòng)實(shí)現(xiàn)初始化某個(gè)方法(init)
這篇文章主要介紹了spring容器啟動(dòng)實(shí)現(xiàn)初始化某個(gè)方法(init),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
解決Java字符串JSON轉(zhuǎn)換異常:cn.hutool.json.JSONException:?Mismatched?
這篇文章主要給大家介紹了關(guān)于如何解決Java字符串JSON轉(zhuǎn)換異常:cn.hutool.json.JSONException:?Mismatched?hr?and?body的相關(guān)資料,文中將解決的辦法通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01
Java利用Spire.PDF for Java將PDF轉(zhuǎn)換為Excel的實(shí)現(xiàn)方法
在Java生態(tài)中,有多種庫(kù)可以處理PDF文件,但要實(shí)現(xiàn)高質(zhì)量的PDF到Excel轉(zhuǎn)換,Spire.PDF for Java是一個(gè)功能全面且性能優(yōu)越的工具,所以本文給大家介紹了Java如何利用Spire.PDF for Java將PDF轉(zhuǎn)換為Excel的實(shí)現(xiàn)方法,需要的朋友可以參考下2025-09-09
Java使用Instant時(shí)輸出的時(shí)間比預(yù)期少了八個(gè)小時(shí)
在Java中,LocalDateTime表示沒(méi)有時(shí)區(qū)信息的日期和時(shí)間,而Instant表示基于UTC的時(shí)間點(diǎn),本文主要介紹了Java使用Instant時(shí)輸出的時(shí)間比預(yù)期少了八個(gè)小時(shí)的問(wèn)題解決,感興趣的可以了解一下2024-09-09
Java使用BigDecimal公式精確計(jì)算及精度丟失問(wèn)題
在工作中經(jīng)常會(huì)遇到數(shù)值精度問(wèn)題,比如說(shuō)使用float或者double的時(shí)候,可能會(huì)有精度丟失問(wèn)題,下面這篇文章主要給大家介紹了關(guān)于Java使用BigDecimal公式精確計(jì)算及精度丟失問(wèn)題的相關(guān)資料,需要的朋友可以參考下2023-01-01
spring?cloud?配置阿里數(shù)據(jù)庫(kù)連接池?druid的示例代碼
這篇文章主要介紹了spring?cloud?配置阿里數(shù)據(jù)庫(kù)連接池?druid,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03

