ThreadLocal設計模式 .

線程安全問題的由來java

  在傳統的Web開發中,咱們處理Http請求最經常使用的方式是經過實現Servlet對象來進行Http請求的響應.Servlet是J2EE的重要標準之一,規定了Java如何響應Http請求的規範.經過HttpServletRequest和HttpServletResponse對象,咱們可以輕鬆地與Web容器交互.編程

  當Web容器收到一個Http請求時,Web容器中的一個主調度線程會從事先定義好的線程中分配一個當前工做線程,將請求分配給當前的工做線程,由該線程來執行對應的Servlet對象中的service方法.當這個工做線程正在執行的時候,Web容器收到另一個請求,主調度線程會一樣從線程池中選擇另一個工做線程來服務新的請求.Web容器自己並不關心這個新的請求是否訪問的是同一個Servlet實例.所以,咱們能夠得出一個結論:對於同一個Servlet對象的多個請求,Servlet的service方法將在一個多線程的環境中併發執行.因此,Web容器默認採用單實例(單Servlet實例)多線程的方式來處理Http請求.這種處理方式可以減小新建Servlet實例的開銷,從而縮短了對Http請求的響應時間.可是,這樣的處理方式會致使變量訪問的線程安全問題.也就是說,Servlet對象並非一個線程安全的對象.安全

下面的測試代碼將證明這一點:
01.package com.qingdao.proxy;  
02.  
03.public class ThreadSafeTestServlet extends HttpServlet {  
04.    // 定義一個實例變量,並不是一個線程安全的變量   
05.    private int counter = 0;  
06.  
07.    public void doGet(HttpServletRequest req, HttpServletResponse resp)  
08.            throws ServletException, IOException {  
09.        doPost(req, resp);  
10.    }  
11.  
12.    public void doPost(HttpServletRequest req, HttpServletResponse resp)  
13.            throws ServletException, IOException {  
14.        // 輸出當前Servlet的信息以及當前線程的信息   
15.        System.out.println(this + ":" + Thread.currentThread());  
16.        // 循環,並增長實例變量counter的值   
17.        for (int i = 0; i < 5; i++) {  
18.            System.out.println("Counter = " + counter);  
19.            try {  
20.                Thread.sleep((long) Math.random() * 1000);  
21.                counter++;  
22.            } catch (InterruptedException exc) {  
23.            }  
24.        }  
25.    }  
26.}  
輸出結果爲:
sample.SimpleServlet@11e1bbf:Thread[http-8081-Processor23,5,main] 
Counter = 60 
Counter = 61 
Counter = 62 
Counter = 65 
Counter = 68 
Counter = 71 
Counter = 74 
Counter = 77 
Counter = 80 
Counter = 83 

sample.SimpleServlet@11e1bbf:Thread[http-8081-Processor22,5,main] 
Counter = 61 
Counter = 63 
Counter = 66 
Counter = 69 
Counter = 72 
Counter = 75 
Counter = 78 
Counter = 81 
Counter = 84 
Counter = 87 

sample.SimpleServlet@11e1bbf:Thread[http-8081-Processor24,5,main] 
Counter = 61 
Counter = 64 
Counter = 67 
Counter = 70 
Counter = 73 
Counter = 76 
Counter = 79 
Counter = 82 
Counter = 85 
Counter = 88

 

 

 
  
經過上面的輸出 , 咱們能夠得出如下三個Servlet對象的運行特性:1. Servlet對象是一個無狀態的單例對象(Singleton), 由於咱們看到屢次請求的this指針所打印出來的hashcode值都相同2. Servlet在不一樣的線程(線程池)中運行,如http-8081-Processor22和http-8081-Processor23等輸出值能夠明顯區分出不一樣的線程執行了同一段Servlet邏輯代碼. 3. Counter變量在不一樣的線程中共享 ,並且它的值被不一樣的線程修改,輸出時已經不是順序輸出.也就是說,其餘的線程會篡改當前線程中實例變量的值,針對這些對象的訪問不是線程安全的.
 
 
 
ThreadLocal模式的實現機理
 
    

在JDK的早期版本中,提供了一種解決多線程併發問題的方案: java.lang.ThreadLocal類.ThreadLocal類在維護變量時,實際使用了當前線程(Thread)中的一個叫作ThreadLocalMap的獨立副本,每一個線程能夠獨立修改屬於本身的副本而不會互相影響,從而隔離了線程和線程,避免了線程訪問實例變量發生衝突的問題.
ThreadLocal自己並非一個線程,而是經過操做當前線程(Thread)中的一個內部變量來達到與其餘線程隔離的目的.之因此取名爲ThreadLocal,所指望表達的含義是其操做的對象是線程(Thread)的一個本地變量.若是咱們看一下Thread的源碼實現,就會發現這一變量,代碼以下:
01.public class Thread implements Runnable {  
02.    // 這裏省略了許多其餘的代碼   
03.    ThreadLocal.ThreadLocalMap threadLocals = null;  
04.} 

這是JDK中Thread源碼的一部分,從中咱們能夠看出ThreadLocalMap跟隨着當前的線程而存在.不一樣的線程Thread,擁有不一樣的ThreadLocalMap的本地實例變量,這也就是「副本」的含義.接下來咱們再來看看ThreadLocal.ThreadLocalMap是如何定義的,以及ThreadLocal如何來操做它數據結構

01.public class ThreadLocal<T> {  
02.  
03.    // 這裏省略了許多其餘代碼   
04.  
05.    // 將value的值保存於當前線程的本地變量中   
06.    public void set(T value) {  
07.        // 獲取當前線程   
08.        Thread t = Thread.currentThread();  
09.        // 調用getMap方法得到當前線程中的本地變量ThreadLocalMap   
10.        ThreadLocalMap map = getMap(t);  
11.        // 若是ThreadLocalMap已存在,直接使用   
12.        if (map != null)  
13.            // 以當前的ThreadLocal的實例做爲key,存儲於當前線程的   
14.            // ThreadLocalMap中,若是當前線程中被定義了多個不一樣的ThreadLocal   
15.            // 的實例,則它們會做爲不一樣key進行存儲而不會互相干擾   
16.            map.set(this, value);  
17.        else  
18.            // ThreadLocalMap不存在,則爲當前線程建立一個新的   
19.            createMap(t, value);  
20.    }  
21.  
22.    // 獲取當前線程中以當前ThreadLocal實例爲key的變量值   
23.    public T get() {  
24.        // 獲取當前線程   
25.        Thread t = Thread.currentThread();  
26.        // 獲取當前線程中的ThreadLocalMap   
27.        ThreadLocalMap map = getMap(t);  
28.        if (map != null) {  
29.            // 獲取當前線程中以當前ThreadLocal實例爲key的變量值   
30.            ThreadLocalMap.Entry e = map.getEntry(this);  
31.            if (e != null)  
32.                return (T) e.value;  
33.        }  
34.        // 當map不存在時,設置初始值   
35.        return setInitialValue();  
36.    }  
37.  
38.    // 從當前線程中獲取與之對應的ThreadLocalMap   
39.    ThreadLocalMap getMap(Thread t) {  
40.        return t.threadLocals;  
41.    }  
42.  
43.    // 建立當前線程中的ThreadLocalMap   
44.    void createMap(Thread t, T firstValue) {  
45.        // 調用構造函數生成當前線程中的ThreadLocalMap   
46.        t.threadLocals = new ThreadLocalMap(this, firstValue);  
47.    }  
48.  
49.    // ThreadLoaclMap的定義   
50.    static class ThreadLocalMap {  
51.        // 這裏省略了許多代碼   
52.    }  
53.}  
 
       
從上述代碼中,咱們看到了ThreadLocal類的大體結構和進行ThreadLocalMap的操做. 咱們能夠從中得出如下的結論:1. ThreadLocalMap變量屬於線程(Thread)的內部屬性,不一樣的線程(Thread)擁有徹底不一樣的ThreadLocalMap變量. 2. 線程(Thread)中的ThreadLocalMap變量的值是在ThreadLocal對象進行set或者get操做時建立的 . 3. 在建立ThreadLocalMap以前 ,會首先檢查當前線程(Thread)中的ThreadLocalMap變量是否已經存在,若是不存在則建立一個;若是已經存在,則使用當前線程(Thread)已建立的ThreadLocalMap. 4. 使用當前線程(Thread)的ThreadLocalMap的關鍵在於使用當前的ThreadLocal的實例做爲key進行存儲 ThreadLocal模式,至少從兩個方面完成了數據訪問隔離,有了橫向和縱向的兩種不一樣的隔離方式, ThreadLocal模式就能真正地作到線程安全:縱向隔離 —— 線程(Thread)與線程(Thread)之間的數據訪問隔離.這一點由線程(Thread)的數據結構保證.由於每一個線程(Thread)在進行對象訪問時,訪問的都是各自線程本身的ThreadLocalMap.橫向隔離 —— 同一個線程中,不一樣的ThreadLocal實例操做的對象之間的相互隔離.這一點由ThreadLocalMap在存儲時,採用當前ThreadLocal的實例做爲key來保證.

 

 

深刻比較TheadLocal模式與synchronized關鍵字多線程

  ThreadLocal模式synchronized關鍵字都用於處理多線程併發訪問變量的問題,只是兩者處理問題的角度和思路不一樣.併發

  1)ThreadLocal是一個java類,經過對當前線程中的局部變量的操做來解決不一樣線程的變量訪問的衝突問題.因此,ThreadLocal提供了線程安全的共享對象機制,每一個線程都擁有其副本.dom

  2)Java中的synchronized是一個保留字,它依靠JVM的鎖機制來實現臨界區的函數或者變量的訪問中的原子性.在同步機制中,經過對象的鎖機制保證同一時間只有一個線程訪問變量.此時,被用做「鎖機制」的變量時多個線程共享的.函數

  同步機制(synchronized關鍵字)採用了「以時間換空間」的方式,提供一份變量,讓不一樣的線程排隊訪問.而ThreadLocal採用了「以空間換時間」的方式,爲每個線程都提供一份變量的副本,從而實現同時訪問而互不影響測試

 

ThreadLocal模式的核心元素this

要完成ThreadLocal模式,其中最關鍵的地方就是建立一個任何地方均可以訪問到的ThreadLocal實例(也就是執行示意圖中的菱形部分).而這一點,咱們能夠經過類的靜態實例變量來實現,這個用於承載靜態實例變量的類就被視做是一個共享環境.咱們來看一個例子,如代碼清單以下所示:

01.public class Counter {  
02.     //新建一個靜態的ThreadLocal變量,並經過get方法將其變爲一個可訪問的對象    
03.     private static ThreadLocal<Integer> counterContext = new ThreadLocal<Integer>(){  
04.         protected synchronized Integer initialValue(){  
05.             return 10;  
06.         }  
07.     };  
08.     // 經過靜態的get方法訪問ThreadLocal中存儲的值   
09.     public static Integer get(){  
10.         return counterContext.get();  
11.     }  
12.     // 經過靜態的set方法將變量值設置到ThreadLocal中   
13.     public static void set(Integer value) {    
14.         counterContext.set(value);    
15.     }   
16.     // 封裝業務邏輯,操做存儲於ThreadLocal中的變量     
17.     public static Integer getNextCounter() {    
18.         counterContext.set(counterContext.get() + 1);    
19.         return counterContext.get();    
20.     }    
21. }  

 

01.public class ThreadLocalTest extends Thread {  
02.     public void run(){  
03.         for(int i = 0; i < 3; i++){    
04.             System.out.println("Thread[" + Thread.currentThread().getName() + "],counter=" + Counter.getNextCounter());    
05.         }    
06.     }  
07. }  
01.public class Test {  
02.     public static void main(String[] args) {  
03.         ThreadLocalTest testThread1 = new ThreadLocalTest();    
04.         ThreadLocalTest testThread2 = new ThreadLocalTest();    
05.         ThreadLocalTest testThread3 = new ThreadLocalTest();    
06.         testThread1.start();    
07.         testThread2.start();    
08.         testThread3.start();  
09.     }  
10. }  

輸出結果:

Thread[Thread-2],counter=11
Thread[Thread-2],counter=12
Thread[Thread-2],counter=13
Thread[Thread-0],counter=11
Thread[Thread-0],counter=12
Thread[Thread-0],counter=13
Thread[Thread-1],counter=11
Thread[Thread-1],counter=12
Thread[Thread-1],counter=13

 

上面的輸出結果也證明了,counter的值在多線程環境中的訪問是線程安全的.從對例子的分析中咱們能夠再次體會到,ThreadLocal模式最合適的使用場景:在同一個線程(Thread)的不一樣開發層次中共享數據.

  從上面的例子中,咱們能夠簡單總結出實現ThreadLocal模式的兩個主要步驟:
  1. 創建一個類,並在其中封裝一個靜態的ThreadLocal變量,
使其成爲一個共享數據環境.
  2. 在類中實現訪問靜態ThreadLocal變量的靜態方法(設值和取值).


  創建在ThreadLocal模式的實現步驟之上,ThreadLocal的使用則更加簡單.在線程執行的任何地方,咱們均可以經過訪問共享數據類中所提供的ThreadLocal變量的設值和取值方法安全地得到當前線程中安全的變量值.
  這兩個步驟,咱們以後會在Struts2的實現中屢次說起,讀者只要能充分理解ThreadLocal處理多線程訪問的基本原理,就能對Struts2的數據訪問和數據共享的設計有一個總體的認識.

  講到這裏,咱們回過頭來看看ThreadLocal模式的引入,到底對咱們的編程模型有什麼重要的意義呢?

  結論 :使用ThreadLocal模式,可使得數據在不一樣的編程層次獲得有效地共享,

  這一點,是由ThreadLocal模式的實現機理決定的.由於實現ThreadLocal模式的一個重要步驟,就是構建一個靜態的共享存儲空間.從而使得任何對象在任什麼時候刻均可以安全地對數據進行訪問.

  結論 使用ThreadLocal模式,能夠對執行邏輯與執行數據進行有效解耦

  這一點是ThreadLocal模式給咱們帶來的最爲核心的一個影響,由於在通常狀況下,Java對象之間的協做關係,主要經過參數和返回值來進行消息傳遞,這也是對象協做之間的一個重要依賴,而ThreadLocal模式完全打破了這種依賴關係,經過線程安全的共享對象來進行數據共享,能夠有效避免在編程層次之間造成數據依賴,這也成爲了XWork事件處理體系設計的核心.

ThreadLocal 多線程共享變量 的隔離
線程內部有ThreadLocalMap 屬性 ,而ThreadLocalMap是ThreadLocal的靜態內部類
橫向 線程與線程之間經過Thread線程內部ThradLocalMap 隔離;
縱向 變量與變量之間經過不一樣ThreadLocal對象隔離
ThreadLocalMap 內部以this指針根據當前ThreadLocal對象爲鍵存儲,實現多個變量的隔離

相關文章
相關標籤/搜索