使用ThreadLocal變量的時機和方法

jopen 9年前發布 | 16K 次閱讀 Java開發 ThreadLocal

并發編程中,一個重要的內容是數據共享。當你創建了實現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變量會在響應客戶端之前被移除,因為當前線程可能被下一個請求重復使用。而 且,如果在使用完畢后不進行清理,它所保持的任何一個對類的引用—這個類會作為部署應用的一部分加載進來—將保留在永久堆棧中,永遠不會被垃圾回收機制回收。

學習愉快!

原文鏈接: howtodoinjava 翻譯: ImportNew.com - Angus
譯文鏈接: http://www.importnew.com/14398.html

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!