可傳輸執行緒本地簡介

banq發表於2024-12-11

在本教程中,我們探索了執行緒區域性變數的不同實現。我們根據需要選擇一種。

簡單的ThreadLocal變數對於具有特定上下文的單執行緒執行非常有用。當我們需要在多個繼承執行緒之間共享上下文時,我們使用InheritableThreadLocal 。最後,我們可以從 transmissiontable-thread-local 庫中選擇TransmittableThreadLocal,以線上程池內的執行緒之間同步上下文更改。

在程式碼執行期間儲存上下文是一項常見的挑戰。例如,我們可能會在 Web 請求期間儲存安全屬性,或者保留可追溯性欄位(如事務 ID)以供記錄或在整個系統中共享。為了處理這個問題,我們可以使用ThreadLocal或InheritableThreadLocal欄位。這些類為我們的上下文提供了一個強大的容器,同時確保了執行緒分離。但是,這些類有侷限性。

在本文中,我們將探討如何使用transmissiontable-thread-local庫中的TransmittableThreadLocal來克服多執行緒問題並安全地管理上下文。

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,我們可以避免多執行緒訪問上下文的問題。我們可以從主執行緒下建立的任何執行緒訪問儲存的值。但是,我們在這裡仍然可能存在限制。如果我們在過程中修改上下文,更新的值將不會出現在並行執行緒中。

讓我們看看它是如何工作的:

@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變數中。然後,我們線上程池執行器中執行的單獨執行緒中檢查該值。我們確認執行緒池中的值與主執行緒中設定的值相匹配。接下來,我們更新變數並再次線上程池中檢查該值。這次我們檢索了之前的值,並且我們的更新被忽略。

使用transmittable-thread-local庫
TransmittableThreadLocal是阿里巴巴開源的 transmissiontable-thread-local 庫中的一個類,它擴充套件了InheritableThreadLocal。它支援跨執行緒共享值,甚至使用執行緒池也是如此。我們可以使用它來確保在執行期間上下文更改在所有執行緒中保持同步。

依賴項
讓我們首先新增必要的依賴項:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.5</version>
</dependency>

新增此依賴項後,我們可以使用TransmittableThreadLocal類。

一個並行執行緒示例
在第一個例子中,我們將檢查TransmittableThreadLocal變數是否可以跨執行緒儲存值:

@Test
void givenTransmittableThreadLocal_whenTryingToGetValueFromAnotherThread_thenValueIsPresent() {
    TransmittableThreadLocal<String> transactionID = new TransmittableThreadLocal<>();
    transactionID.set(UUID.randomUUID().toString());
    new Thread(() -> assertNotNull(transactionID.get())).start();
}

我們建立了一個事務ID並在另一個執行緒中成功檢索到它的值。

ExecutorService示例
在下一個示例中,我們將建立一個TransmittableThreadLocal變數,其具有事務 ID。然後,我們將它提交到執行緒池並在過程中對其進行修改:

@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。此類允許我們線上程池中的執行緒之間傳輸執行緒本地狀態。

並行流示例
使用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 包裝器來同步主執行緒和並行流執行期間使用的所有執行緒之間的上下文。

在我們的實驗中,我們在主執行緒中建立並修改了事務 ID。此外,我們從並行流中訪問了此事務 ID。我們成功檢索了初始值和修改後的值

 

相關文章