程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java多線程基礎總結四:ThreadLocal

Java多線程基礎總結四:ThreadLocal

編輯:關於JAVA

說到ThreadLocal,首先說說這個類的命名。直觀上看好像是個Thread的什麼親戚,但其實它想表達的意思是線程本地變量,也就是說每 個線程自己的變量。它作為一個JDK5以後支持范型的類,主要是想利用范型把非線程安全的共享變量,封裝成綁定線程的安全不共享變量。 這樣的解釋我想我們多半能猜出它的實現思路:把一個共享變量在每個線程使用時,初始化一個副本,並且和線程綁定。以後所有的線程對 共享變量的操作都是對線程內部那個副本,完全的線程內部變量的操作。

要實現這樣功能類的設計,主要技術點是要能把副本和線程綁定映射,程序可以安全查找到當前線程的副本,修改後安全的綁定給線程 。所以我們想到了Map的存儲結構,ThreadLocal內部就是使用了線程安全的Map形式的存儲把currentThread和變量副本一一映射。

既然要把共享的變成不共享的,那麼就要變量滿足一個場景:變量的狀態不需要共享。例如無狀態的bean在多線程之間是安全的,因為 線程之間不需要同步bean的狀態,用了就走(很不負責啊),想用就用。但是對於有狀態的bean在線程之間則必須小心,線程A剛看到狀態 是a,正想利用a做事情,線程B把bean的狀態改為了b,結果做了不該做的。但是如果有狀態的bean不需要共享狀態,每個線程看到狀態a或 者b都可以做出自己的行為,這種情況下不同步的選擇就是ThreadLocal了。

利用ThreadLocal的優勢就在於根本不用擔心有狀態的bean為了狀態的一致而犧牲性能,去使用synchronized限制只有一個線程在同一時 間做出關於bean狀態的行為。而是多個線程同時根據自己持有的bean的副本的狀態做出行為,這樣的轉變對於並發的支持是那麼的不可思議 。例如一個 Dao內有個Connection的屬性,當多個線程使用Dao的同一個實例時,問題就來了:多個線程用一個Connection,而且它還是有 連接,關閉等等的狀態轉變的,我們很敏感的想到這個屬性不安全!再看這個屬性,其實它是多麼的想告訴線程哥哥們:我的這些狀態根本 就不想共享,不要因為我的狀態而不敢一起追求。線程哥哥們也郁悶:你要是有多胞胎姐妹該多好啊!這時候ThreadLocal大哥過來說:小 菜,我來搞定!你們這些線程一人一個 Connection,你想關就關,想連接就連接,再也不用抱怨說它把你的連接關了。這樣Dao的實例再也 不用因為自己有個不安全的屬性而自卑了。當然 ThreadLocal的思路雖然是很好的,但是官方的說法是最初的實現性能並不好,隨著Map結 構和Thread.currentThread的改進,性能較之synchronized才有了明顯的優勢。所以要是使用的是JDK1.2,JDK1.3等等,也不要妄想麻雀變 鳳凰...

再看ThreadLocal和synchronized的本質。前者不在乎多占點空間,但是絕對的忍受不了等待;後者對等待無所謂,但是就是不喜歡浪費 空間。這也反映出了算法的一個規律:通常是使用場景決定時間和空間的比例,既省時又省地的算法多數情況下只存在於幻想之中。下面寫 個簡單的例子解釋一下,不過個人覺得設計的例子不太好,以後有實際的啟發再替換吧。

Java代碼

import java.util.concurrent.atomic.AtomicInteger;

/** 
  * User: yanxuxin 
  * Date: Dec 14, 2009 
  * Time: 9:26:41 PM 
  */
public class ThreadLocalSample extends Thread {
   private OperationSample2 operationSample;

   public ThreadLocalSample(OperationSample2 operationSample) {
     this.operationSample = operationSample;
   }

   @Override
   public void run() {
     operationSample.printAndIncrementNum();
   }

   public static void main(String[] args) {

     final OperationSample2 operation = new OperationSample2();//The shared Object for threads.

     for (int i = 0; i < 5; i++) {
       new ThreadLocalSample(operation).start();
     }
   }
}

class OperationSample {
   private int num;

   //public synchronized void printAndIncrementNum() {
   public void printAndIncrementNum() {
     for (int i = 0; i < 2; i++) {
       System.out.println(Thread.currentThread().getName() + "[id=" + num + "]");
       num += 10;
     }
   }
}

class OperationSample2 {

   private static ThreadLocal<Integer> threadArg = new ThreadLocal<Integer>() {
     @Override
     protected Integer initialValue() {
       return 0;
     }
   };

   public void printAndIncrementNum() {
     for (int i = 0; i < 2; i++) {
       int num = threadArg.get();
       threadArg.set(num + 10);
       System.out.println(Thread.currentThread().getName() + "[id=" + num + "]");
     }
   }
}

class OperationSample3 {

   private static final AtomicInteger uniqueId = new AtomicInteger(0);
   private static ThreadLocal<Integer> threadArg = new ThreadLocal<Integer>() {
     @Override
     protected Integer initialValue() {
       return uniqueId.getAndIncrement();
     }
   };

   public void printAndIncrementNum() {
     for (int i = 0; i < 2; i++) {
       int num = threadArg.get();
       threadArg.set(num + 10);
       System.out.println(Thread.currentThread().getName() + "[id=" + num + "]");
     }
   }
}

這個例子中ThreadLocalSample繼承自Thread持有OperationSample三個版本中的一個引用,並且在線程運行時執行 printAndIncrementNum()方法。

首先看版本1:OperationSample有個共享變量num,printAndIncrementNum()方法沒有同步保護,方法就是循環給 num賦新值並打印改變 值的線程名。因為沒有任何的同步保護,所以原本打算每個線程打印出的值是相鄰遞加10的結果變成了不確定的遞加。有可能線程1的循環 第一次打印0,第二次就打印50。這時候我們使用被注釋的方法聲明,結果就是預想的同一個線程的兩次結果是相鄰的遞加,因為同一時刻 只有一個線程獲得 OperationSample實例的隱式鎖完成循環釋放鎖。

再看版本2:假設我們有個遞增10的簡單計數器,但是是對每個線程的計數。也就是說我們有一個Integer計數器負責每個線程的計數。 雖然它是有狀態的,會變的,但是因為每個線程之間不需要共享變化,所以可以用ThreadLocal管理這個Integer。在這裡看到我們的 ThreadLocal變量的initialValue()方法被覆寫了,這個方法的作用就是當調用ThreadLocal的get()獲取線程綁定的副本時如果還沒綁定則 調用這個方法在Map中添加當前線程的綁定映射。這裡我們返回0,表示每個線程的初始副本在ThreadLocal的Map的紀錄都是0。再看 printAndIncrementNum()方法,沒有任何的同步保護,所以多個線程可以同時進入。但是,每個線程通過threadArg.get() 拿到的僅僅是自 己的Integer副本,threadArg.set(num + 10)的也是自己的副本值。所以結果就是雖然線程的兩次循環打印有快有慢,但是每個線程的兩次 結果都是0和10。

最後是版本3:和版本2的不同在於新加了一個uniqueId的變量。這個變量是java.util.concurrent.atomic包下的原子變量類。這是基於 硬件支持的CAS(比較交換)原語的實現,所以保證了++,--,+=,-=等操作的原子性。所以在ThreadLocal變量的 initialValue()方法中使用 uniqueId.getAndIncrement()將為每個線程初始化唯一不會重復的遞加1的Integer 副本值。而結果就會變成5個線程的首次打印是0~4的5 個數字,第二次每個線程的打印是線程對應的首次數字加10的值。

對於ThreadLocal的使用,Spring的源碼中有大量的應用,主要是要支持Singleton的實例管理,那麼自身的一些Singleton的實現內非線 程安全的變量,屬性要用ThreadLocal隔離共享。同時我們在使用Spring的IOC時也要注意有可能多線程調用的注冊到IOC容器的 Singleton 型實例是否真的線程安全。另外java.util.concurrent.atomic內的原子變量類簡單的提了一下,再看看怎麼能瞎編出東西來吧。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved