CodeGym /課程 /JAVA 25 SELF /Scoped Values 與新的執行緒機制(Java 21+)

Scoped Values 與新的執行緒機制(Java 21+)

JAVA 25 SELF
等級 57 , 課堂 4
開放

1. 為什麼 ThreadLocal 逐漸失去時效

ThreadLocal 到底是用來做什麼的?

在傳統的多執行緒環境中,執行緒通常存活時間較長(例如在伺服器上),有時需要為每個執行緒保存其專屬的資料——這些資料不應與其他執行緒互相干擾。比如,使用者名稱、請求 ID,或暫存的緩衝區。

為了達成這點,Java 提供了 ThreadLocal<T>——相當於執行緒的「個人空間」,可以存放不影響其他執行緒的資料:

ThreadLocal<String> user = new ThreadLocal<>();

user.set("Alice"); // 該值只會儲存在此執行緒中
String name = user.get(); // 在這裡會回傳 "Alice",在其他執行緒中則為 null

為什麼 ThreadLocal 與虛擬執行緒不合

虛擬執行緒的生命週期與傳統「重量級」執行緒截然不同。它們會以成千上萬的數量快速建立與消亡——有時甚至在不到毫秒的時間內。而 ThreadLocal 會把資料綁在特定的執行緒上,彷彿該執行緒會永遠存在。

當虛擬執行緒結束時,它在 ThreadLocal 中的資料可能仍懸掛在記憶體中——即使執行緒早已終止。這會導致洩漏,因為 JVM 並不總是知道這些值已經不再需要。

如果執行緒被重複利用(例如使用執行緒池),還可能出現更糟的情況:先前的「他人」上下文會意外地流到新的請求中。想像一下,用戶 Petya 取得了用戶 Vasya 的資料——問題與漏洞接踵而至。

ThreadLocal 很適合用在執行緒不多且存活很久的情境。但對於虛擬執行緒而言——就像試著把東西存放在每秒都會消失的衣櫃裡。

2. Scoped Values:傳遞上下文的新方式

Scoped Values 是 Java 21 引入的新工具,用更優雅的方式解決了 ThreadLocal 的老問題。它不是像 ThreadLocal 那樣把資料存進執行緒裡,而是把資料「附著」在執行範圍(scope)上——也就是特定的一段程式碼。值只會在該段程式執行期間存在,結束後會自動消失,不會在記憶體中留下痕跡。

import java.lang.ScopedValue;

ScopedValue<String> USER = ScopedValue.newInstance();

ScopedValue.where(USER, "Alice").run(() -> {
    System.out.println("Hello, " + USER.get()); // 輸出:Hello, Alice
});

當程式碼離開 run 區塊後,該值就不再可用——嘗試存取會拋出例外。無需手動清理任何東西。

Scoped Values 不會污染記憶體、不會在執行緒間混淆上下文,還允許建立巢狀區域,內層值可以暫時覆蓋外層值。這是傳遞上下文時乾淨、可預測且安全的方式,特別適合虛擬執行緒的世界。

3. Scoped Values 的使用範例

範例 1:傳遞使用者上下文

假設我們有一個伺服器會處理不同使用者的請求。對於每個請求,我們都希望知道是誰發起的。

import java.lang.ScopedValue;

public class ServerExample {
    static final ScopedValue<String> USER = ScopedValue.newInstance();

    public static void main(String[] args) {
        processRequest("Alice");
        processRequest("Bob");
    }

    static void processRequest(String userName) {
        ScopedValue.where(USER, userName).run(() -> {
            handleBusinessLogic();
        });
    }

    static void handleBusinessLogic() {
        System.out.println("為使用者處理:" + USER.get());
    }
}

會發生什麼事:

  • 每個請求都會建立自己的 scope,在其中 USER 分別等於「Alice」或「Bob」。
  • handleBusinessLogic() 內,我們總能取得正確的使用者名稱。
  • 一旦請求處理結束,該值就會消失。

範例 2:帶有上下文的日誌紀錄

假設我們想在日誌中自動帶入請求識別碼:

import java.lang.ScopedValue;

public class LoggingExample {
    static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

    public static void main(String[] args) {
        for (int i = 1; i <= 3; i++) {
            String reqId = "REQ-" + i;
            ScopedValue.where(REQUEST_ID, reqId).run(() -> {
                log("開始處理");
                doWork();
                log("結束處理");
            });
        }
    }

    static void log(String message) {
        System.out.printf("[%s] %s%n", REQUEST_ID.get(), message);
    }

    static void doWork() {
        log("處理中...");
    }
}

結果(範例):

[REQ-1] 開始處理
[REQ-1] 處理中...
[REQ-1] 結束處理
[REQ-2] 開始處理
[REQ-2] 處理中...
[REQ-2] 結束處理
[REQ-3] 開始處理
[REQ-3] 處理中...
[REQ-3] 結束處理

每個 scope 都保存自己的請求識別碼,執行緒之間不會混淆。

4. Scoped Values 與虛擬執行緒:天作之合

為什麼 Scoped Values 在虛擬執行緒中特別有用

虛擬執行緒存活時間很短——它們會以成千上萬的數量被建立與銷毀,有時只需一瞬間。因此,舊的 ThreadLocal 做法(把資料「綁死」在執行緒上)在這裡並不適用:執行緒消失得太快,而上下文可能意外洩漏或被混用。

ScopedValue 則把資料綁定在任務本身——也就是其執行範圍。這表示上下文(例如使用者名稱或請求 ID)會跟著程式碼走,而不是跟著執行緒走。當任務結束時,值會自動消失。對虛擬執行緒來說,這是安全、乾淨且沒有驚喜的理想解決方案。

範例:使用虛擬執行緒的大量任務處理

import java.lang.ScopedValue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VirtualThreadScopedValueDemo {
    static final ScopedValue<Integer> TASK_ID = ScopedValue.newInstance();

    public static void main(String[] args) {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

        for (int i = 1; i <= 10_000; i++) {
            int taskId = i;
            executor.submit(() -> ScopedValue.where(TASK_ID, taskId).run(() -> {
                processTask();
            }));
        }

        executor.shutdown();
    }

    static void processTask() {
        // 每個任務都有自己的 TASK_ID
        System.out.println("正在處理任務 #" + TASK_ID.get());
    }
}

重點:

  • 每個任務都會建立屬於它的 TASK_ID scope。
  • 即使任務並行執行,值也不會在執行緒間混淆。
  • 不會有記憶體洩漏:scope 會隨任務一起結束。

5. 比較:ThreadLocal vs ScopedValue

比較項 ThreadLocal ScopedValue
綁定方式 綁在執行緒 綁在程式區域(scope)
生命週期 執行緒存活期間 scope 執行期間
安全性 有洩漏與混淆風險 無洩漏、無混淆
虛擬執行緒 效率差且風險高 非常適合
使用方式
set/get
where(...).run(...), get
巢狀性 不支援覆寫 可在內層覆蓋外層值

6. 巢狀區域(scopes):覆蓋值

ScopedValue<String> INFO = ScopedValue.newInstance();

ScopedValue.where(INFO, "外層").run(() -> {
    System.out.println(INFO.get()); // "外層"
    ScopedValue.where(INFO, "內層").run(() -> {
        System.out.println(INFO.get()); // "內層"
    });
    System.out.println(INFO.get()); // "外層"
});

結果:

外層
內層
外層

這很方便,例如在同一個任務內需要暫時覆寫上下文值時。

Scoped Values:常見使用情境

  • 傳遞使用者或請求的識別碼:用於記錄日誌或檢查權限。
  • 日誌紀錄:自動把上下文帶入日誌。
  • 追蹤:用於偵錯與效能剖析。
  • 交易參數:例如隔離等級或運作模式。
  • 任何只在單一任務(或其子任務)範圍內可見的「上下文」。

7. 其他新機制:Structured Concurrency

Structured Concurrency 是一種做法:將相關的任務(例如同一操作的子流程)視為一個整體來管理——如果母任務結束或失敗,所有子任務會自動被取消。這能降低「被遺忘」或「懸掛」執行緒的風險。

範例(非常簡化):

try (var scope = StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> result1 = scope.fork(() -> fetchData1());
    Future<String> result2 = scope.fork(() -> fetchData2());

    scope.join(); // 等待兩者完成
    scope.throwIfFailed(); // 若任一失敗 — 拋出例外

    String combined = result1.resultNow() + result2.resultNow();
    System.out.println(combined);
}

優點:

  • 更乾淨地管理任務生命週期。
  • 沒有「懸掛」的子流程。
  • 錯誤處理更容易。

Structured Concurrency 目前仍在預覽階段,但已在積極演進中。

8. 實務建議與限制

何時使用 Scoped Values?

  • 需要在任務之間傳遞上下文時,特別是在使用虛擬執行緒時。
  • 如果你先前使用 ThreadLocal——不妨考慮改用 ScopedValue

何時仍需要 ThreadLocal?

  • 少數情況下,執行緒存活時間非常長,且上下文需要在整個生命週期內保持「常駐」(例如面對舊系統時)。

限制

  • Scoped Values 在建立 scope 後不可變——它們是唯讀的。
  • Scoped Values 不能在 scope 外使用:在區域外嘗試取得值會拋出例外。
  • 不要用 Scoped Values 存放大型物件——區域應該是輕量且快速的。

9. 使用 Scoped Values 的常見錯誤

錯誤 #1:嘗試在 scope 之外取得值。 如果在 ScopedValue.where(...) 區塊之外呼叫 USER.get(),你會得到 NoSuchElementException。請確認取用只發生在區域內。

錯誤 #2:嘗試在 scope 內修改值。 Scoped Values 並不是可變容器。若需要暫時「覆蓋」某個值,請建立巢狀的 scope。

錯誤 #3:同時使用 ThreadLocal 與 ScopedValue。 除非萬不得已,不要混用這兩種機制——這可能導致上下文混亂與錯誤。

錯誤 #4:忘記把邏輯包進 run() 區塊。 如果你只寫了 ScopedValue.where(USER, "Alice") 而沒有 .run(() -> { ... }),是不會建立任何 scope 的!

錯誤 #5:試圖用 Scoped Value 保存長期的全域資訊。 此類目的更適合使用一般變數,或在合理情況下使用 ThreadLocal

1
問卷/小測驗
Virtual Threads,等級 57,課堂 4
未開放
Virtual Threads
Virtual Threads
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION