可傳輸線程本地簡介
1. 概述
在程式碼執行期間儲存上下文是一個常見的挑戰。例如,我們可能會在 Web 請求期間儲存安全屬性,或保留交易 ID 等可追溯性字段,以便在系統中進行記錄或共用。為了處理這個問題,我們可以使用ThreadLocal
或InheritableThreadLocal
欄位。這些類別為我們的上下文提供了強大的容器,同時確保線程分離。然而,這些類別有其局限性。
在本文中,我們將探討如何使用Transmissiontable-thread-local庫中的TransmittableThreadLocal
來克服多執行緒問題並安全地管理上下文。
2. ThreadLocal
問題
我們可以使用ThreadLocal
來儲存呼叫上下文。但是,如果我們嘗試從另一個執行緒存取它,我們將無法獲得該值。讓我們來看一個簡單的例子來說明這個問題:
@Test
void givenThreadLocal_whenTryingToGetValueFromAnotherThread_thenNullIsExpected() {
ThreadLocal<String> transactionID = new ThreadLocal<>();
transactionID.set(UUID.randomUUID().toString());
new Thread(() -> assertNull(transactionID.get())).start();
}
我們在主線程中設定 UUID 並在新線程中檢索它。正如預期的那樣,我們沒有得到該值。
InheritableThreadLocal
問題
透過使用[InheritableThreadLocal](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/InheritableThreadLocal.html) ,
我們可以避免多執行緒存取上下文的問題。我們可以從主線程下創建的任何線程存取儲存的值。然而,我們在這裡仍然可能有局限性。如果我們在此過程中修改上下文,則更新的值將不會出現在平行執行緒中。
讓我們看看它是如何運作的:
@Test
void givenInheritableThreadLocal_whenChangeTheTransactionIdAfterSubmissionToThreadPool_thenNewValueWillNotBeAvailableInParallelThread() {
String firstTransactionIDValue = UUID.randomUUID().toString();
InheritableThreadLocal<String> transactionID = new InheritableThreadLocal<>();
transactionID.set(firstTransactionIDValue);
Runnable task = () -> assertEquals(firstTransactionIDValue, transactionID.get());
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(task);
String secondTransactionIDValue = UUID.randomUUID().toString();
Runnable task2 = () -> assertNotEquals(secondTransactionIDValue, transactionID.get());
transactionID.set(secondTransactionIDValue);
executorService.submit(task2);
executorService.shutdown();
}
我們建立一個 UUID 值並將其設定在InheritableThreadLocal
變數中。然後,我們檢查線程池執行器中運行的單獨線程中的值。我們確認線程池內的值與主線程中設定的值相符。接下來,我們更新變數並再次檢查線程池中的值。這次我們檢索以前的值,並且我們的更新被忽略。
4. 使用可傳輸線程本地庫
TransmittableThreadLocal
是阿里巴巴開源的可傳輸線程本地庫中的一個類,它擴展了InheritableThreadLocal
。它支援跨線程共享值,甚至可以使用線程池。我們可以使用它來確保上下文變更在執行期間在所有執行緒之間保持同步。
4.1.依賴關係
讓我們先加入必要的依賴項:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.5</version>
</dependency>
新增此依賴項後,我們可以使用TransmittableThreadLocal
類別。
4.2.一個並行線程範例
在第一個範例中,我們將檢查TransmittableThreadLocal
變數是否可以跨執行緒儲存值:
@Test
void givenTransmittableThreadLocal_whenTryingToGetValueFromAnotherThread_thenValueIsPresent() {
TransmittableThreadLocal<String> transactionID = new TransmittableThreadLocal<>();
transactionID.set(UUID.randomUUID().toString());
new Thread(() -> assertNotNull(transactionID.get())).start();
}
我們建立一個事務 ID 並在另一個執行緒中成功檢索其值。
4.3. ExecutorService
範例
在下一個範例中,我們將建立一個帶有交易 ID 的TransmittableThreadLocal
變數。然後,我們將其提交到線程池並在過程中修改它:
@Test
void givenTransmittableThreadLocal_whenChangeTheTransactionIdAfterSubmissionToThreadPool_thenNewValueWillBeAvailableInParallelThread() {
String firstTransactionIDValue = UUID.randomUUID().toString();
String secondTransactionIDValue = UUID.randomUUID().toString();
TransmittableThreadLocal<String> transactionID = new TransmittableThreadLocal<>();
transactionID.set(firstTransactionIDValue);
Runnable task = () -> assertEquals(firstTransactionIDValue, transactionID.get());
Runnable task2 = () -> assertEquals(secondTransactionIDValue, transactionID.get());
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(TtlRunnable.get(task));
transactionID.set(secondTransactionIDValue);
executorService.submit(TtlRunnable.get(task2));
executorService.shutdown();
}
我們可以看到初始值和修改後的值已成功檢索。我們這裡使用TtlRunnable
。該類別允許我們在線程池中的線程之間傳輸線程本地狀態。
4.4.並行流程範例
使用TransmittableThreadLocal
變數的另一個有趣的案例涉及並行流。當我們的流中有多個項目時,它可能在ForkJoinPool
上執行。這可能會導致池中所有執行緒的共享上下文出現問題。讓我們看看如何使用TransmittableThreadLocal
解決這個挑戰:
@Test
void givenTransmittableThreadLocal_whenChangeTheTransactionIdAfterParallelStreamAlreadyProcessed_thenNewValueWillBeAvailableInTheSecondParallelStream() {
String firstTransactionIDValue = UUID.randomUUID().toString();
String secondTransactionIDValue = UUID.randomUUID().toString();
TransmittableThreadLocal<String> transactionID = new TransmittableThreadLocal<>();
transactionID.set(firstTransactionIDValue);
TtlExecutors.getTtlExecutorService(new ForkJoinPool(4))
.submit(
() -> List.of(1, 2, 3, 4, 5)
.parallelStream()
.forEach(i -> assertEquals(firstTransactionIDValue, transactionID.get())));
transactionID.set(secondTransactionIDValue);
TtlExecutors.getTtlExecutorService(new ForkJoinPool(4))
.submit(
() -> List.of(1, 2, 3, 4, 5)
.parallelStream()
.forEach(i -> assertEquals(secondTransactionIDValue, transactionID.get())));
}
由於我們無法修改用於所有並行線程的共享線程池,因此我們需要在單獨的ThreadPoolExecutor
內運行流程。我們使用[TtlExecutors](https://github.com/noseew/transmittable-thread-local-fork/blob/master/ttl-core/src/main/java/com/alibaba/ttl3/executor/TtlExecutors.java)
包裝器來同步主執行緒和平行流執行期間使用的所有執行緒之間的上下文。
在我們的實驗中,我們在主執行緒中建立並修改了事務 ID。此外,我們也從平行流中存取了該交易 ID。我們成功檢索了初始值和修改後的值。
5. 結論
在本教程中,我們探索了線程局部變數的不同實作。我們根據自己的需求選擇一種。
簡單的ThreadLocal
變數對於具有特定上下文的單執行緒執行非常有用。當我們需要在多個繼承執行緒之間共用上下文時,我們使用InheritableThreadLocal
。最後,我們可以從 Transmissiontable-Thread-Local 函式庫中選擇TransmittableThreadLocal
來同步執行緒池中執行緒之間的上下文變更。
與往常一樣,程式碼可以在 GitHub 上取得。