Java 25 Scoped Values:面試如何比較 ThreadLocal 與結構化作用域?
題幹與適用情境
面試官可能會問:「Java 25 的 ScopedValue 解決了什麼問題?請比較它與 ThreadLocal,並說明線上程池或虛擬執行緒中的正確用法。」
JEP 506 在 JDK 25 將 Scoped Values 定稿。它讓呼叫方在詞法作用域內向深層方法和子執行緒共享不可變資料,減少為傳遞上下文而層層增加參數的需要。題目考察生命週期、可見性和並發邊界,不能只回答「ThreadLocal 的新名字」。
面試官考察點
- 是否理解 ScopedValue 是不可變、受作用域控制的隱式參數。
- 是否能說明
where(...).run(...)或call(...)的繫結範圍和恢復行為。 - 是否識別它與可變 ThreadLocal、執行緒池重用和虛擬執行緒繼承的差異。
- 是否考慮 key 的存取控制、繫結數量、例外傳播和取消。
- 是否知道何時仍應保留 ThreadLocal 或顯式參數。
回答前需要釐清的問題
- 共享的是請求 ID、認證主體、租戶,還是可變交易狀態?
- 資料必須唯讀嗎,子工作是否需要繼承?
- 執行模型是平台執行緒、虛擬執行緒、結構化並發,還是現有執行緒池?
- 程式碼是否可能在作用域外讀取 key,或把 Carrier 洩露給不受信呼叫方?
- 現有框架是否依賴 ThreadLocal 的清理、可變更新或 MDC 整合?
30 秒回答架構
可以這樣回答:
ScopedValue 是 Java 25 的不可變、詞法作用域上下文機制。呼叫方用ScopedValue.where(key, value).run(...)或call(...)建立繫結,作用域結束後自動恢復;只有持有 key 的程式碼能讀取它。相比 ThreadLocal,它減少執行緒池清理和可變狀態洩漏,適合請求上下文、虛擬執行緒和結構化並發。它不取代需要逐步更新的 ThreadLocal,也不能跨出作用域保存參照。實作前我會驗證子工作繼承、例外和取消行為,並控制 key 的存取權限。
分步深入解答
先理解繫結模型
ScopedValue 的值在動態執行範圍內可讀,但繫結由程式結構明確界定:
static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
ScopedValue.where(REQUEST_ID, "req-42").run(() -> {
audit("start");
handleRequest();
});
static void audit(String message) {
logger.info("{} {}", REQUEST_ID.orElse("missing"), message);
}audit 不必接收請求 ID 參數,但只有在繫結作用域中才能讀取有意義的值;作用域結束後,繫結不會繼續污染呼叫執行緒。
對比 ThreadLocal 的可變性
ThreadLocal 允許每條執行緒持有可變值,線上程池重用時必須清理,否則下一個請求可能讀到舊上下文。ScopedValue 的設計目標是共享不可變值,並透過巢狀繫結覆蓋外層值,再在作用域退出後恢復。它減少清理責任,但不能承載需要在深層程式碼中反覆 set 的狀態機。
處理子執行緒與虛擬執行緒
JEP 506 關注與虛擬執行緒和結構化並發配合的成本與可推理性。是否自動繼承、繼承何時發生以及任務執行器的行為必須以目標 JDK 和 API 契約驗證,不能把普通執行緒池的經驗直接套用。共享物件本身仍應不可變,避免透過參照繞過上下文邊界。
設計 key、例外和相容路徑
key 應是私有 static final 物件或受控 API 的一部分,不要把可被任意模組持有的全域 key 當作安全邊界。讀取缺失值時選擇 orElse 或顯式例外;orElse 的參數在 Java 25 不能是 null。例外離開 run 或 call 時,作用域會結束並恢復外層繫結。需要相容舊 JDK 時保留顯式參數或 ThreadLocal 實作,並用測試矩陣驗證行為。
高品質示範回答
我會把 ScopedValue 視為不可變的隱式參數,而不是新的可變 ThreadLocal。請求入口用where(key, value).run或call建立結構化作用域,深層方法透過持有的 key 讀取請求 ID、租戶或認證主體;退出作用域後繫結自動恢復。它適合虛擬執行緒和結構化並發中的唯讀上下文,避免執行緒池重用造成的清理遺漏。ThreadLocal 仍適合必須逐步更新、且框架已圍繞它建立的狀態,但要在 finally 中清理。實作時我會限制 key 可見性,驗證子工作繼承、例外、取消和巢狀覆蓋,並在舊 JDK 上保留顯式參數或相容實作。
常見錯誤
- 把 ScopedValue 說成自動清理的可變 ThreadLocal。
- 在作用域外保存 Carrier、值或讀取 key,導致生命週期錯誤。
- 忽略執行緒池重用、子工作繼承和虛擬執行緒差異。
- 使用可變物件作為值,卻宣稱整個上下文不可變。
- 讓任意模組共享 key,把存取控制誤當成資料加密。
- 忘記 Java 25
orElse不接受 null,或沒有舊 JDK 相容策略。
追問及應對
1. ScopedValue 能取代所有 ThreadLocal 嗎?
不能。它適合唯讀、作用域明確的上下文;需要在呼叫鏈中逐步更新的狀態仍可使用 ThreadLocal 或顯式參數,並承擔清理責任。
2. 巢狀繫結會發生什麼?
內層作用域可以暫時繫結同一 key 的新值,內層退出後恢復外層值。回答時應說明例外路徑也必須退出作用域,不能把內層值洩露到外層。
3. 如何測試並發繼承?
分別覆蓋平台執行緒、虛擬執行緒、結構化任務和執行緒池重用;檢查子工作看到的值、取消後的清理、巢狀覆蓋和作用域外讀取。測試應固定 JDK 版本與執行器設定。