使用ThreadLocal變數的時機和方法

ImportNew - Angus發表於2015-02-04

併發程式設計中,一個重要的內容是資料共享。當你建立了實現Runnable介面的執行緒,然後開啟使用相同Runnable例項的各種Thread物件,所有 的執行緒便共享定義在Runnable物件中的屬性。也就是說,當你在一個執行緒中改變任意屬性時,所有的執行緒都會因此受到影響,同時會看到第一個執行緒修改後的值。有時我們希望如此,比如:多個執行緒增大或減小同一個計數器變數;但是,有時我們希望確保每個執行緒,只能工作在它自己的執行緒例項的拷貝上,同時不會影 響其他執行緒的資料。

使用ThreadLocal的時機

舉個例子,想象你在開發一個電子商務應用,你需要為每一個控制器處理的顧客請求,生成一個唯一的事務ID,同時將其傳到管理器或DAO的業務方法中,以便記錄日誌。一種方案是將事務ID作為一個引數,傳到所有的業務方法中。但這並不是一個好的方案,它會使程式碼變得冗餘。

你可以使用ThreadLocal型別的變數解決這個問題。首先在控制器或者任意一個前處理器攔截器中生成一個事務ID,然後在ThreadLocal中 設定事務ID,最後,不論這個控制器呼叫什麼方法,都能從threadlocal中獲取事務ID。而且這個應用的控制器可以同時處理多個請求,同時在框架 層面,因為每一個請求都是在一個單獨的執行緒中處理的,所以事務ID對於每一個執行緒都是唯一的,而且可以從所有執行緒的執行路徑獲取。

擴充套件閱讀:與JAX-RS ResteasyProviderFactory共享上下文資料(ThreadLocalStack例項)

ThreadLocal類

Java併發API為使用ThreadLocal類的區域性執行緒變數提供了一個簡潔高效的機制,

public class ThreadLocal<T> extends Object {...}

這個類提供了一個區域性執行緒變數。這些變數不同於其所對應的常規變數,對於常規變數,每個執行緒只能訪問(通過get或set方法)其自身所擁有的,獨立初始化變數拷貝。在一個類中,ThreadLocal型別的例項是典型的私有、靜態private static)欄位,因為我們可以將其作為執行緒的關聯狀態(比如:使用者ID或者事務ID)

這個類有以下方法:

  1. get():返回當前執行緒拷貝的區域性執行緒變數的值。
  2. initialValue():返回當前執行緒賦予區域性執行緒變數的初始值。
  3. remove():移除當前執行緒賦予區域性執行緒變數的值。
  4. set(T value):為當前執行緒拷貝的區域性執行緒變數設定一個特定的值。

怎樣使用ThreadLocal?

下面的例子使用兩個區域性執行緒變數,即threadId和startDate。它們都遵循推薦的定義方法,即“private static”型別的欄位。threadId用來區分當前正在執行的執行緒,startDate用來獲取執行緒開啟的時間。上面的資訊將列印到控制檯,以此驗 證每一個執行緒管理他自己的變數拷貝。

class DemoTask implements Runnable {

   // Atomic integer containing the next thread ID to be assigned
   private static final AtomicInteger nextId = new AtomicInteger(0);

   // Thread local variable containing each thread's ID
   private static final ThreadLocal<Integer> threadId =
        new ThreadLocal<Integer>() {
            @Override
            protected Integer initialValue() {
               return nextId.getAndIncrement();
            }
         };

   // Returns the current thread's unique ID, assigning it if necessary
   public int getThreadId() {
      return threadId.get();
   }

   // Returns the current thread's starting timestamp
   private static final ThreadLocal<Date> startDate =
       new ThreadLocal<Date>() {
           protected Date initialValue() {
               return new Date();
           }
       };

   @Override
   public void run() {
      System.out.printf("Starting Thread: %s : %sn",
                        getThreadId(), startDate.get());
      try {
         TimeUnit.SECONDS.sleep((int) Math.rint(Math.random() * 10));
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
      System.out.printf("Thread Finished: %s : %sn",
                        getThreadId(), startDate.get());
   }
}

現在要驗證變數本質上能夠維持其自身狀態,而與多執行緒的多次初始化無關。我們首先需要建立執行這個任務的三個執行緒,然後開啟執行緒,接著驗證它們列印到控制檯中的資訊。

Starting Thread: 0 : Wed Dec 24 15:04:40 IST 2014
Thread Finished: 0 : Wed Dec 24 15:04:40 IST 2014

Starting Thread: 1 : Wed Dec 24 15:04:42 IST 2014
Thread Finished: 1 : Wed Dec 24 15:04:42 IST 2014

Starting Thread: 2 : Wed Dec 24 15:04:44 IST 2014
Thread Finished: 2 : Wed Dec 24 15:04:44 IST 2014

在上面的輸出中,列印出的宣告序列每次都在變化。我已經把它們放到了序列中,這樣對於每一個執行緒例項,我們都可以清楚地辨別出,區域性執行緒變數保持著安全狀態,而絕不會混淆。自己嘗試下!

區域性執行緒通常使用在這樣的情況下,當你有一些物件並不滿足執行緒安全,但是你想避免在使用synchronized關鍵字、塊時產生的同步訪問,那麼,讓每個執行緒擁有它自己的物件例項。

注意:區域性變數是同步或區域性執行緒的一個好的替代,它總是能夠保證執行緒安全。唯一可能限制你這樣做的是你的應用設計約束。

警告:在webapp伺服器上,可能會保持一個執行緒池,那麼ThreadLocal變數會在響應客戶端之前被移除,因為當前執行緒可能被下一個請求重複使用。而 且,如果在使用完畢後不進行清理,它所保持的任何一個對類的引用—這個類會作為部署應用的一部分載入進來—將保留在永久堆疊中,永遠不會被垃圾回收機制回收。

相關文章