線程本地變量ThreadLocal


一、本地線程變量使用場景

     並發應用的一個關鍵地方就是共享數據。如果你創建一個類對象,實現Runnable接口,然后多個Thread對象使用同樣的Runnable對象,全部的線程都共享同樣的屬性。這意味着,如果你在一個線程里改變一個屬性,全部的線程都會受到這個改變的影響。

     有時,你希望程序里的各個線程的屬性不會被共享。 Java 並發 API提供了一個很清楚的機制叫本地線程變量即ThreadLocal

     模擬ThreadLocal類實現:線程范圍內的共享變量,每個線程只能訪問他自己的,不能訪問別的線程。

二、對ThreadLocal的理解

      ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做本地線程變量,其實意思差不多。

      ThreadLocal和本地線程沒有半毛錢關系,更不是一個特殊的Thread,它只是一個線程的局部變量(其實就是一個Map用於存儲每一個線程的變量副本,Map中元素的Key為線程對象,而Value對應線程的變量副本),ThreadLocal會為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。

      對於多線程資源共享的問題,同步機制(Synchronized)采用了“以時間換空間”的方式,而ThreadLocal采用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。

      官方對ThreadLocal的描述:

 

    

  

      1、每個線程都有自己的局部變量

          每個線程都有一個獨立於其他線程的上下文來保存這個變量,一個線程的本地變量對其他線程是不可見的(有前提,后面解釋)

      2、獨立於變量的初始化副本

          ThreadLocal可以給一個初始值,而每個線程都會獲得這個初始化值的一個副本,這樣才能保證不同的線程都有一份拷貝。

      3、狀態與某一個線程相關聯

        ThreadLocal 不是用於解決共享變量的問題的,不是為了協調線程同步而存在,而是為了方便每個線程處理自己的狀態而引入的一個機制,理解這點對正確使用ThreadLocal至關重要。

      通過ThreadLocal存取的數據,總是與當前線程相關,也就是說,JVM 為每個運行的線程,綁定了私有的本地實例存取空間,從而為多線程環境常出現的並發訪問問題提供了一種隔離機制。

   我們還是先來看一個例子:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ConnectionManager {
     
     private static Connection connect = null ;
     
     public static Connection openConnection() {
         if (connect == null ){
             connect = DriverManager.getConnection();
         }
         return connect;
     }
     
     public static void closeConnection() {
         if (connect!= null )
             connect.close();
     }
}

   假設有這樣一個數據庫鏈接管理類,這段代碼在單線程中使用是沒有任何問題的,但是如果在多線程中使用呢?很顯然,在多線程中使用會存在線程安全問題:第一,這里面的2個方法都沒有進行同步,很可能在openConnection方法中會多次創建connect;第二,由於connect是共享變量,那么必然在調用connect的地方需要使用到同步來保障線程安全,因為很可能一個線程在使用connect進行數據庫操作,而另外一個線程調用closeConnection關閉鏈接。

  所以出於線程安全的考慮,必須將這段代碼的兩個方法進行同步處理,並且在調用connect的地方需要進行同步處理。

  這樣將會大大影響程序執行效率,因為一個線程在使用connect進行數據庫操作的時候,其他線程只有等待。

  那么大家來仔細分析一下這個問題,這地方到底需不需要將connect變量進行共享?事實上,是不需要的。假如每個線程中都有一個connect變量,各個線程之間對connect變量的訪問實際上是沒有依賴關系的,即一個線程不需要關心其他線程是否對這個connect進行了修改的。

  到這里,可能會有朋友想到,既然不需要在線程之間共享這個變量,可以直接這樣處理,在每個需要使用數據庫連接的方法中具體使用時才創建數據庫鏈接,然后在方法調用完畢再釋放這個連接。比如下面這樣:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ConnectionManager {
     
     private  Connection connect = null ;
     
     public Connection openConnection() {
         if (connect == null ){
             connect = DriverManager.getConnection();
         }
         return connect;
     }
     
     public void closeConnection() {
         if (connect!= null )
             connect.close();
     }
}
 
 
class Dao{
     public void insert() {
         ConnectionManager connectionManager = new ConnectionManager();
         Connection connection = connectionManager.openConnection();
         
         //使用connection進行操作
         
         connectionManager.closeConnection();
     }
}

   這樣處理確實也沒有任何問題,由於每次都是在方法內部創建的連接,那么線程之間自然不存在線程安全問題。但是這樣會有一個致命的影響:導致服務器壓力非常大,並且嚴重影響程序執行性能。由於在方法中需要頻繁地開啟和關閉數據庫連接,這樣不盡嚴重影響程序執行效率,還可能導致服務器壓力巨大。

  那么這種情況下使用ThreadLocal是再適合不過的了,因為ThreadLocal在每個線程中對該變量會創建一個副本,即每個線程內部都會有一個該變量,且在線程內部任何地方都可以使用,線程之間互不影響,這樣一來就不存在線程安全問題,也不會嚴重影響程序執行性能。

  但是要注意,雖然ThreadLocal能夠解決上面說的問題,但是由於在每個線程中都創建了副本,所以要考慮它對資源的消耗,比如內存的占用會比不使用ThreadLocal要大。

三、深入解析ThreadLocal類

      在上面談到了對ThreadLocal的一些理解,那我們下面來看一下具體ThreadLocal是如何實現的。

  先了解一下ThreadLocal類提供的幾個方法:

?
1
2
3
4
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }

   get()方法是用來獲取ThreadLocal在當前線程中保存的變量副本;

       set()用來設置當前線程中變量的副本;

       remove()用來移除當前線程中變量的副本;

       initialValue()是一個protected方法,一般是用來在使用時進行重寫的,它是一個延遲加載方法;

       ThreadLocal是如何為每個線程創建變量的副本的:

        1、在每個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,Key為當前ThreadLocal變量,value為變量副本(即T類型的變量)。

   2、初始時,在Thread里面,threadLocals為空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,並且以當前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals。

  3、在當前線程里面,如果要使用副本變量,就可以通過get方法在threadLocals里面查找。

    下面通過一個例子來證明通過ThreadLocal能達到在每個線程中創建變量副本的效果:

public class Test {
     ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
     ThreadLocal<String> stringLocal = new ThreadLocal<String>();
 
     
     public void set() {
         longLocal.set(Thread.currentThread().getId());
         stringLocal.set(Thread.currentThread().getName());
     }
     
     public long getLong() {
         return longLocal.get();
     }
     
     public String getString() {
         return stringLocal.get();
     }
     
     public static void main(String[] args) throws InterruptedException {
         final Test test = new Test();
         
         
         test.set();
         System.out.println(test.getLong());
         System.out.println(test.getString());
     
         
         Thread thread1 = new Thread(){
             public void run() {
                 test.set();
                 System.out.println(test.getLong());
                 System.out.println(test.getString());
             };
         };
         thread1.start();
         thread1.join();
         
         System.out.println(test.getLong());
         System.out.println(test.getString());
     }
}

   這段代碼的輸出結果為:

  

  從這段代碼的輸出結果可以看出,在main線程中和thread1線程中,longLocal保存的副本值和stringLocal保存的副本值都不一樣。最后一次在main線程再次打印副本值是為了證明在main線程中和thread1線程中的副本值確實是不同的。

總結一下:

  1)實際的通過ThreadLocal創建的副本是存儲在每個線程自己的threadLocals中的;

  2)為何threadLocals的類型ThreadLocalMap的鍵值為ThreadLocal對象,因為每個線程中可有多個threadLocal變量,就像上面代碼中的longLocal和stringLocal;

 

四.ThreadLocal的應用場景

   最常見的ThreadLocal使用場景為:用來解決數據庫連接、Session管理,多線程單例模式訪問;

    訂單處理包含一系列操作:減少庫存量、增加一條流水台賬、修改總賬,這幾個操作要在同一個事務中完成,通常也即同一個線程中進行處理,如果累加公司應收款的操作失敗了,則應該把前面的操作回滾,否則,提交所有操作,這要求這些操作使用相同的數據庫連接對象,而這些操作的代碼分別位於不同的模塊類中。

    銀行轉賬包含一系列操作: 把轉出帳戶的余額減少,把轉入帳戶的余額增加,這兩個操作要在同一個事務中完成,它們必須使用相同的數據庫連接對象,轉入和轉出操作的代碼分別是兩個不同的帳戶對象的方法。

   我們先看一個簡單的例子:

public class ThreadLocalTest {
         
         //創建一個Integer型的線程本地變量
     public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>() {
         @Override
         protected Integer initialValue() {
             return 0 ;
         }
     };
     public static void main(String[] args) throws InterruptedException {
         Thread[] threads = new Thread[ 5 ];
         for ( int j = 0 ; j < 5 ; j++) {       
                threads[j] = new Thread( new Runnable() {
                 @Override
                 public void run() {
                                         //獲取當前線程的本地變量,然后累加5次
                     int num = local.get();
                     for ( int i = 0 ; i < 5 ; i++) {
                         num++;
                     }
                                         //重新設置累加后的本地變量
                     local.set(num);
                     System.out.println(Thread.currentThread().getName() + " : " + local.get());
 
                 }
             }, "Thread-" + j);
         }
 
         for (Thread thread : threads) {
             thread.start();
         }
     }
}
運行后結果: 

Thread-0 : 5

Thread-4 : 5

Thread-2 : 5

Thread-1 : 5

Thread-3 : 5

我們看到,每個線程累加后的結果都是5,各個線程處理自己的本地變量值,線程之間互不影響。

如:數據庫連接:

Session連接:

五、ThreadLocal使用的一般步驟
 
1、在多線程的類(如ThreadDemo類)中,創建一個ThreadLocal對象threadXxx,用來保存線程間需要隔離處理的對象xxx。
2、在ThreadDemo類中,創建一個獲取要隔離訪問的數據的方法getXxx(),在方法中判斷,若ThreadLocal對象為null時候,應該new()一個隔離訪問類型的對象,並強制轉換為要應用的類型。
3、在ThreadDemo類的run()方法中,通過getXxx()方法獲取要操作的數據,這樣可以保證每個線程對應一個數據對象,在任何時刻都操作的是這個對象。
相關鏈接請參考:http://my.oschina.net/xianggao/blog/392440?fromerr=nKHw4fBT


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM