ThreadLocal 使用手冊 | 按需收藏

張哥說技術發表於2023-01-12

一、背景

為了使 Java 中的一個變數的值在任何給定的時間點上都能跨越不同的執行緒,開發人員必須使用 Java 程式語言提供的同步機制,如Synchronized或鎖物件。

這可以確保在任何時候只有一個執行緒獲得訪問權,確保在使用那些有可能出現爭用問題的區域內的變數時,多個執行緒的併發訪問不會產生衝突。進入ThreadLocal

Java 中的ThreadLocal類允許程式設計師建立只有建立這些變數的執行緒才能訪問的變數。這對於建立執行緒安全的程式碼很有用,因為它確保每個執行緒都有自己的變數副本,並且不能干擾其他執行緒。

這意味著在你的應用程式中執行的每個執行緒都會有自己的變數副本。在這個程式設計教程中,我們將瞭解與ThreadLocal類相關的基本概念,它的好處,它的工作原理,以及如何在 Java 應用程式中使用它。

二、Java 中的執行緒安全

在 Java 中實現執行緒安全的方法有很多種,每種方法都有其優缺點:

  • Synchronized 程式碼塊或方法。這是最基本的執行緒安全形式,在某些情況下它是有效的。然而,如果不小心使用,它也會導致效能問題。
  • 原子變數。這些是可以原子方式讀寫的變數,不需要同步。你可以利用 Java 中的 ThreadLocal 來減少同步化的成本。
  • 不可變的物件。如果一個物件的狀態一旦建立就不能改變,那麼它就被稱為不可變的。這通常與其他方法一起使用,如同步方法或原子變數。
  • 鎖物件。你可以利用這些物件來鎖定一大塊程式碼,從而使這塊程式碼在某一特定時刻只允許被一個執行緒訪問。與同步程式碼塊或方法相比,它們能夠實現更好的細粒度控制,但也可能導致更復雜的程式碼。

在 Java 中實現執行緒安全的方法有很多,每種方法都有其優點和缺點。

三、Java 中的 ThreadLocal 是什麼?

ThreadLocal是 Java 中的一個特殊類,它透過提供每個執行緒的上下文併為每個執行緒單獨維護它們來幫助我們實現執行緒安全。換句話說,ThreadLocal是一個 Java 類,可以用來定義只由建立它們的執行緒訪問的變數。這在很多情況下都很有用,但最常見的使用情況是,你需要儲存不線上程之間共享的資料。

例如,假設一個開發者正在編寫一個多執行緒的應用程式,每個執行緒需要有自己的變數副本。如果你只是簡單地使用一個普通的變數,有可能一個執行緒會在另一個執行緒有機會使用它之前就覆蓋了該變數的值。有了ThreadLocal,每個執行緒都有自己的變數副本,所以不存在一個執行緒在另一個執行緒有機會使用它之前就覆蓋了該值的風險。

一個ThreadLocal例項在需要儲存執行緒特定資訊的 Java 類中被表示為一個私有靜態欄位。ThreadLocal變數不是全域性變數,所以它們不能被其他執行緒訪問,除非它們被明確傳遞給其他執行緒。這使得它們成為儲存敏感資訊的理想選擇,如密碼或使用者 ID,它們不應該被其他執行緒訪問。

3.1 什麼時候使用 ThreadLocal?

在 Java 中使用ThreadLocal有幾個原因。最常見的用例是當你需要為一個給定的執行緒維護狀態資訊,但該狀態線上程之間是不可共享的。例如,如果你使用一個 JDBC 連線池,每個執行緒都需要它的連線。在這種情況下,使用ThreadLocal允許每個執行緒擁有自己的連線,而不必擔心每次建立或銷燬執行緒時建立和銷燬連線的開銷。

ThreadLocal的另一個常見用例是當你需要在一個執行緒中的不同元件之間共享狀態資訊時。例如,如果你有一個服務需要呼叫多個 DAO(資料庫訪問物件),每個 DAO 可能需要其ThreadLocal變數來儲存當前的事務或會話資訊。允許每個元件透過ThreadLocal訪問它所需要的狀態,而不必擔心元件之間的資料傳遞。

最後,你也可以使用ThreadLocal作為一個簡單的方法來為一個執行緒建立全域性變數。這對於除錯或記錄的場景通常是有用的。例如,你可以建立一個ThreadLocal變數來儲存當前的使用者 ID。你將輕鬆地記錄該使用者執行的所有操作,而不必到處傳遞使用者 ID。

四、ThreadLocal 基礎用法

4.1 建立一個 ThreadLocal

建立ThreadLocal例項就像建立任何其他 Java 物件一樣 - 透過new  運算子。

private ThreadLocal threadLocal = new ThreadLocal();

這每個執行緒中只需要做一次。多個執行緒可以在這個 ThreadLocal 中獲取和設定值,而每個執行緒將只看到它自己設定的值。

4.2 設定 ThreadLocal 值

一旦一個ThreadLocal被建立,你可以使用它的set()方法來設定要儲存在其中的值。

threadLocal.set("一個執行緒本地值");

4.3 獲取 ThreadLocal 值

使用ThreadLocalget()方法讀取儲存在其中的值。

String threadLocalValue = (String) threadLocal.get();

4.4 刪除 ThreadLocal 值

可以刪除在 ThreadLocal 變數中設定的值。可以透過呼叫remove()方法來刪除一個值。

threadLocal.remove();

4.5 刪除所有ThreadLocal變數的值

最後,您可以呼叫clear() 方法來刪除所有ThreadLocal變數的值。這通常僅在開發人員的程式關閉時才需要。例如,要清除所有ThreadLocal變數,可以使用以下程式碼:

threadLocal.clear();

注意:原文中描述的此方法,在 JDK8\17\18 中其實均未找到,讀者老師支付寶小程式團隊也有留言提出此疑惑,希望瞭解情況的讀者老師煩請留言解惑。關於如何清理的問題,在 stackoverflow 中有看到一些有意思的方案討論how-to-clean-up-threadlocals,後續會翻譯整理出來。

小結

重要的是要注意ThreadLocal例項中的資料只能由建立它的執行緒訪問。

五、ThreadLocal 高階用法

5.1 泛型 ThreadLocal

您可以使用泛化型別建立一個。使用泛型型別只能將泛型型別的物件設定為ThreadLocal的值.  此外,你不需要對get()返回的值進行型別轉換。下面是一個泛型 ThreadLocal 的例子。

private ThreadLocal<String> myThreadLocal = new ThreadLocal<String>();

現在您只能在ThreadLocal例項中儲存字串。此外,你不需要對從ThreadLocal獲得的值進行型別轉換:

myThreadLocal.set("Hello ThreadLocal");

String threadLocalValue = myThreadLocal.get();

5.2 初始 ThreadLocal 值

可以為一個 Java ThreadLocal設定一個初始值,除非被set()新的值,否則 get() 的總是這個初始值。你有兩個選擇來為 ThreadLocal 指定一個初始值。

  • 建立一個 ThreadLocal 子類,重寫 initialValue()方法。
  • 建立一個具有Supplier介面實現的 ThreadLocal。我將在下面的章節中向你展示這兩種選擇。

1) Override initialValue()

為 Java ThreadLocal變數指定初始值的第一種方法是建立一個 ThreadLocal的子類,重寫其initialValue()方法。建立ThreadLocal子類的最簡單方法是簡單地建立一個匿名子類,就在你建立ThreadLocal變數的地方。下面是一個建立ThreadLocal的匿名子類的例子,它覆蓋了initialValue()方法。

private ThreadLocal myThreadLocal = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
        return String.valueOf(System.currentTimeMillis());
    }
};

注意,不同的執行緒仍然會看到不同的初始值。每個執行緒將建立自己的初始值。只有當你從initialValue()方法中返回完全相同的物件時,所有執行緒才能看到相同的物件。然而,首先使用ThreadLocal的全部意義在於避免不同執行緒看到相同的例項。

2)Supplier 實現

為 Java ThreadLocal變數指定初始值的第二種方法是使用其靜態工廠方法withInitial(Supplier),並將Supplier介面的實現作為引數傳遞給它。這個Supplier實現為ThreadLocal提供初始值。下面是一個使用其靜態工廠方法withInitial()建立ThreadLocal的例子,其中傳遞了一個簡單的Supplier實現作為引數。

ThreadLocal<String> threadLocal = ThreadLocal.withInitial(new Supplier<String>() {
    @Override
    public String get() {
        return String.valueOf(System.currentTimeMillis());
    }
});

由於Supplier是一個功能介面,它可以用 Java Lambda 表示式來實現。下面是將Supplier的實現作為一個 lambda 表示式提供給withInitial()的樣子。

ThreadLocal threadLocal = ThreadLocal.withInitial(
        () -> { return String.valueOf(System.currentTimeMillis()); } );

正如你所看到的,這比前面的例子要短一些。但它還可以更短一些,使用最密集的 lambda 表示式的語法。

ThreadLocal threadLocal3 = ThreadLocal.withInitial(
        () -> String.valueOf(System.currentTimeMillis()) );

5.3 ThreadLocal 延遲初始化

在某些情況下,你不能使用設定初始值的標準方法。例如,也許你需要一些配置資訊,而這些資訊在你建立ThreadLocal變數時是不可用的。在這種情況下,你可以延遲地設定初始值。下面是一個例子,說明如何在 Java ThreadLocal上延遲設定初始值。

public class MyDateFormatter {

    private ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<>();

    public String format(Date date) {
        SimpleDateFormat simpleDateFormat = getThreadLocalSimpleDateFormat();
        return simpleDateFormat.format(date);
    }


    private SimpleDateFormat getThreadLocalSimpleDateFormat() {
        SimpleDateFormat simpleDateFormat = simpleDateFormatThreadLocal.get();
        if(simpleDateFormat == null) {
            simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            simpleDateFormatThreadLocal.set(simpleDateFormat);
        }
        return simpleDateFormat;
    }
}

注意format()方法是如何呼叫getThreadLocalSimpleDateFormat()方法來獲得一個 Java SimpleDatFormat例項的。如果在ThreadLocal中沒有設定SimpleDateFormat例項,就會建立一個新的SimpleDateFormat,並在ThreadLocal變數中設定。一旦一個執行緒在ThreadLocal變數中設定了自己的SimpleDateFormat,同一個SimpleDateFormat物件就會被用於該執行緒。但只適用於該執行緒。每個執行緒都會建立自己的SimpleDateFormat例項,因為它們不能看到彼此在ThreadLocal變數中設定的例項。

SimpleDateFormat類不是執行緒安全的,所以多個執行緒不能同時使用它。為了解決這個問題,上面的MyDateFormatter類為每個執行緒建立了一個SimpleDateFormat,所以每個呼叫format()方法的執行緒將使用它自己的SimpleDateFormat例項。

5.4 Inheritable ThreadLocal

InheritableThreadLocal類是ThreadLocal的一個子類。InheritableThreadLocal不是讓每個執行緒在ThreadLocal中擁有自己的值,而是讓一個執行緒和由該執行緒建立的所有子執行緒都能獲得值。下面是一個完整的 Java InheritableThreadLocal例子。

public class InheritableThreadLocalBasicExample {

    public static void main(String[] args) {

        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        InheritableThreadLocal<String> inheritableThreadLocal =
                new InheritableThreadLocal<>();

        Thread thread1 = new Thread(() -> {
            System.out.println("===== Thread 1 =====");
            threadLocal.set("Thread 1 - ThreadLocal");
            inheritableThreadLocal.set("Thread 1 - InheritableThreadLocal");

            System.out.println(threadLocal.get());
            System.out.println(inheritableThreadLocal.get());

            Thread childThread = new Thread( () -> {
                System.out.println("===== ChildThread =====");
                System.out.println(threadLocal.get());
                System.out.println(inheritableThreadLocal.get());
            });
            childThread.start();
        });

        thread1.start();

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("===== Thread2 =====");
            System.out.println(threadLocal.get());
            System.out.println(inheritableThreadLocal.get());
        });
        thread2.start();
    }
}

這個例子建立了一個普通的 Java ThreadLocal和一個 Java InheritableThreadLocal。然後,這個例子建立了一個執行緒來設定ThreadLocalInheritableThreadLocal的值--然後建立一個子執行緒來訪問ThreadLocalInheritableThreadLocal的值。只有InheritableThreadLocal的值對子執行緒是可見的。

最後,這個例子建立了第三個執行緒,它也試圖訪問ThreadLocalInheritableThreadLocal - 但它沒有看到第一個執行緒儲存的任何值。

執行這個例子的輸出結果是這樣的。

===== Thread 1 =====
Thread 1 - ThreadLocal
Thread 1 - InheritableThreadLocal
===== ChildThread =====
null
Thread 1 - InheritableThreadLocal
===== Thread2 =====
null
null

六、使用 Java 的 ThreadLocal 的優點和缺點

如果使用得當,Java 中的ThreadLocal類可以減少同步的開銷並提高效能。透過消除記憶體洩漏,可以更輕鬆地閱讀和維護程式碼。

當程式設計師需要維護特定於單個執行緒的狀態時,當他們需要透過減少同步來提高效能時,以及當他們需要防止記憶體洩漏時,他們可以使用ThreadLocal變數。

與使用ThreadLocal變數相關的一些缺點包括競爭條件和記憶體洩漏。

如何防止競爭條件

在使用ThreadLocal變數時,沒有保證能防止競賽條件,因為它們本身就很容易出現競賽條件。然而,有一些最佳實踐可以幫助減少發生競賽條件的可能性,例如使用原子操作,並確保對ThreadLocal變數的所有訪問都適當地同步。

七、關於 Java 中 ThreadLocal 的最終思考

ThreadLocal是 Java 中的一個強大的 API,它允許開發人員儲存和檢索特定於某個執行緒的資料。換句話說,ThreadLocal允許你定義只有建立這些變數的執行緒才能訪問的變數。

如果使用得當,ThreadLocal可以成為建立高效能、執行緒安全的程式碼的寶貴工具。然而,在你的 Java 應用程式中使用ThreadLocal之前,必須意識到使用它的潛在風險和弊端。

八、最後說一句

我是石頁兄,如果這篇文章對您有幫助,或者有所啟發的話,歡迎關注筆者的微信公眾號【 架構染色 】進行交流和學習。您的支援是我堅持寫作最大的動力。


整理自英文原文:
  • https://jenkov.com/tutorials/java-concurrency/threadlocal.html
  • https://www.developer.com/java/java-threadlocal/



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2931861/,如需轉載,請註明出處,否則將追究法律責任。

相關文章