Java併發編程:深刻剖析ThreadLocal

1、對ThreadLocal的理解java

  ThreadLocal,不少地方叫作線程本地變量,也有些地方叫作線程本地存儲,其實意思差很少。可能不少朋友都知道ThreadLocal爲變量在每一個線程中都建立了一個副本,那麼每一個線程能夠訪問本身內部的副本變量。這句話從字面上看起來很容易理解,可是真正理解並非那麼容易。數據庫

ThreadLocal的官方API解釋爲:編程

"該類提供了線程局部 (thread-local) 變量。這些變量不一樣於它們的普通對應物,由於訪問某個變量(經過其 get 或 set 方法)的每一個線程都有本身的局部變量,它獨立於變量的初始化副本。ThreadLocal 實例一般是類中的 private static 字段,它們但願將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。"數組

大概的意思有兩點:安全

 一、ThreadLocal提供了一種訪問某個變量的特殊方式:訪問到的變量屬於當前線程,即保證每一個線程的變量不同,而同一個線程在任何地方拿到的變量都是一致的,這就是所謂的線程隔離。服務器

二、若是要使用ThreadLocal,一般定義爲private static類型,在我看來最好是定義爲private static final類型。數據結構

     不少博客都這樣說:ThreadLocal爲解決多線程程序的併發問題提供了一種新的思路;ThreadLocal的目的是爲了解決多線程訪問資源時的共享問題。若是你也這樣認爲的,那如今給你10秒鐘,清空以前對ThreadLocal的錯誤的認知!多線程

     ThreadLocal能夠總結爲一句話:ThreadLocal的做用是提供線程內的局部變量,這種變量在線程的生命週期內起做用,減小同一個線程內多個函數或者組件之間一些公共變量的傳遞的複雜度。併發

咱們仍是先來看一個例子:ide

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進行了修改的。

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

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要大。

2、深刻解析ThreadLocal類

在上面談到了對ThreadLocal的一些理解,那咱們下面來看一下具體ThreadLocal是如何實現的。先了解一下ThreadLocal類提供的幾個方法:

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }

     get()方法是用來獲取ThreadLocal在當前線程中保存的變量副本,set()用來設置當前線程中變量的副本,remove()用來移除當前線程中變量的副本,initialValue()是一個protected方法,用來返回此線程局部變量的當前線程的初始值,通常是在使用時進行重寫的,它是一個延遲加載方法,下面會詳細說明。

首先咱們來看一下ThreadLocal類是如何爲每一個線程建立一個變量的副本的。先看下get方法的實現:

 1 public T get() {
 2     //1.首先獲取當前線程
 3     Thread t = Thread.currentThread();
 4     //2.獲取線程的map對象
 5     ThreadLocalMap map = getMap(t);
 6     //3.若是map不爲空,以threadlocal實例爲key獲取到對應Entry,而後從Entry中取出對象便可。
 7     if (map != null) {
 8         ThreadLocalMap.Entry e = map.getEntry(this);
 9         if (e != null)
10             return (T)e.value;
11     }
12     //若是map爲空,也就是第一次沒有調用set直接get(或者調用過set,又調用了remove)時,爲其設定初始值
13     return setInitialValue();
14 }

     第3行是取得當前線程,而後經過getMap(t)方法獲取到一個map,map的類型爲ThreadLocalMap。而後接着下面獲取到<key,value>鍵值對,注意這裏獲取鍵值對傳進去的是  this,而不是當前線程t。若是獲取成功,則返回value值。若是map爲空,則調用setInitialValue方法返回value。

   下面咱們對上面的每一句來仔細分析。首先看一下getMap方法中作了什麼:

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

可能你們沒有想到的是,在getMap中,是調用當期線程t,返回當前線程t中的一個成員變量threadLocals。那麼咱們繼續取Thread類中取看一下成員變量threadLocals是什麼:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

實際上就是一個ThreadLocalMap,這個類型是ThreadLocal類的一個內部類,咱們繼續取看ThreadLocalMap的實現:

能夠看到ThreadLocalMap的Entry繼承了WeakReference,而且使用ThreadLocal做爲鍵值。

     總結一下,get()方法的第3和第5行很明顯是獲取屬於當前線程的ThreadLocalMap,若是這個map不爲空,咱們就以當前的ThreadLocal爲鍵,去獲取相應的Entry,Entry是ThreadLocalMap的靜態內部類,它繼承於弱引用,因此在get()方法裏面如第10行同樣調用e.value方法就能夠獲取實際的資源副本值。可是若是有一個爲空,說明屬於該線程的資源副本還不存在,則須要去建立資源副本,從代碼中能夠看到是調用setInitialValue()方法,其定義以下:

 1 /**
 2  * Variant of set() to establish initialValue. Used instead
 3  * of set() in case user has overridden the set() method.
 4  *
 5  * @return the initial value
 6  */
 7 private T setInitialValue() {
 8     T value = initialValue();
 9     Thread t = Thread.currentThread();
10     ThreadLocalMap map = getMap(t);
11     if (map != null)
12         map.set(this, value);
13     else
14         createMap(t, value);
15     return value;
16 }

     第8行調用initialValue()方法初始化一個值。接下來是判斷線程的ThreadLocalMap是否爲空,不爲空就直接設置值(鍵爲this,值爲value),爲空則建立一個Map,調用方法爲createMap(),其定義以下:

1 void createMap(Thread t, T firstValue) {
2     t.threadLocals = new ThreadLocalMap(this, firstValue);
3 }

簡單明瞭,而ThreadLocalMap的這個構造方法的實現以下:

/**
 * Construct a new map initially containing (firstKey, firstValue).
 * ThreadLocalMaps are constructed lazily, so we only create
 * one when we have at least one entry to put in it.
 */
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

實例化table數組用於存儲鍵值對,而後經過映射將鍵值對存儲進入相應的位置。

下面再來看set方法。

/** 
 * Sets the current thread's copy of this thread-local variable 
 * to the specified value.  Most subclasses will have no need to 
 * override this method, relying solely on the {@link #initialValue} 
 * method to set the values of thread-locals. 
 * 
 * @param value the value to be stored in the current thread's copy of 
 *        this thread-local. 
 */  
public void set(T value) {  
    // 獲取當前線程對象  
    Thread t = Thread.currentThread();  
    // 獲取當前線程本地變量Map  
    ThreadLocalMap map = getMap(t);  
    // map不爲空  
    if (map != null)  
        // 存值  
        map.set(this, value);  
    else  
        // 建立一個當前線程本地變量Map  
        createMap(t, value);  
}

     在這個方法內部咱們看到,首先經過getMap(Thread t)方法獲取一個和當前線程相關的ThreadLocalMap,而後將變量的值設置到這個ThreadLocalMap對象中,固然若是獲取到的ThreadLocalMap對象爲空,就經過createMap方法建立。     

     至此,可能大部分朋友已經明白了ThreadLocal是如何爲每一個線程建立變量的副本的:

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

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

   而後在當前線程裏面,若是要使用副本變量,就能夠經過get方法在threadLocals裏面查找。

3、示例

示例1:

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

package com.demo.test;

public class TestThreadLocal {

    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 TestThreadLocal test = new TestThreadLocal();
         
        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());
    }
}

這段代碼的輸出結果爲:

1
main
8
Thread-0
1
main

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

總結一下:

1)實際的經過ThreadLocal建立的副本是存儲在每一個線程本身的threadLocals中的;

2)爲什麼threadLocals的類型ThreadLocalMap的鍵值爲ThreadLocal對象,由於每一個線程中可有多個threadLocal變量,就像上面代碼中的longLocal和stringLocal;

3)在進行get以前,必須先set,不然會報空指針異常。 若是想在get以前不須要調用set就能正常訪問的話,必須重寫initialValue()方法。

     由於在上面的代碼分析過程當中,咱們發現若是沒有先set的話,即在map中查找不到對應的存儲,則會經過調用setInitialValue方法返回i,而在setInitialValue方法中,有一個語句是T value = initialValue(), 而默認狀況下,initialValue方法返回的是null。

注意 :默認狀況下 initValue(), 返回 null 。線程在沒有調用 set 以前,第一次調用 get 的時候, get方法會默認去調用 initValue 這個方法。因此若是沒有覆寫這個方法,可能致使 get 返回的是 null 。固然若是調用過 set 就不會有這種狀況了。可是每每在多線程狀況下咱們不能保證每一個線程的在調用 get 以前都調用了set ,因此最好對 initValue 進行覆寫,以避免致使空指針異常。

看下面這個例子:

package com.demo.test;

public class TestThreadLocal {

    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 TestThreadLocal test = new TestThreadLocal();
         
        //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線程中,沒有先set,直接get的話,運行時會報空指針異常。

Exception in thread "main" java.lang.NullPointerException
    at com.demo.test.TestThreadLocal.getLong(TestThreadLocal.java:14)
    at com.demo.test.TestThreadLocal.main(TestThreadLocal.java:25)

可是若是改爲下面這段代碼,即重寫了initialValue方法:

package com.demo.test;

public class TestThreadLocal {

    ThreadLocal<Long> longLocal = new ThreadLocal<Long>(){
         protected Long initialValue() {
             return Thread.currentThread().getId();
         };
    };
    ThreadLocal<String> stringLocal = new ThreadLocal<String>(){
         protected String initialValue() {
             return Thread.currentThread().getName();
         };
    };
 
    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 TestThreadLocal test = new TestThreadLocal();
         
        //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());
    }
}

就能夠直接不用先set而直接調用get了。

示例2:

package com.demo.test;

public class TestNum {

    // ①經過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值  
    private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {  
        public Integer initialValue() {  
            return 0;  
        }  
    };  
  
    // ②獲取下一個序列值  
    public int getNextNum() {  
        seqNum.set(seqNum.get() + 1);  
        return seqNum.get();  
    }  
  
    public static void main(String[] args) {  
        TestNum sn = new TestNum();  
        // ③ 3個線程共享sn,各自產生序列號  
        TestClient t1 = new TestClient(sn);  
        TestClient t2 = new TestClient(sn);  
        TestClient t3 = new TestClient(sn);  
        t1.start();  
        t2.start();  
        t3.start();  
    }  
  
    private static class TestClient extends Thread {  
        private TestNum sn;  
  
        public TestClient(TestNum sn) {  
            this.sn = sn;  
        }  
  
        public void run() {  
            for (int i = 0; i < 3; i++) {  
                // ④每一個線程打出3個序列值  
                System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["  
                         + sn.getNextNum() + "]");  
            }  
        }  
    }  
}

      一般咱們經過匿名內部類的方式定義ThreadLocal的子類,提供初始的變量值,如例子中①處所示。TestClient線程產生一組序列號,在③處,咱們生成3個TestClient,它們共享同一個TestNum實例。運行以上代碼,在控制檯上輸出如下的結果:

thread[Thread-0] --> sn[1]
thread[Thread-1] --> sn[1]
thread[Thread-2] --> sn[1]
thread[Thread-1] --> sn[2]
thread[Thread-0] --> sn[2]
thread[Thread-1] --> sn[3]
thread[Thread-2] --> sn[2]
thread[Thread-0] --> sn[3]
thread[Thread-2] --> sn[3]

     考察輸出的結果信息,咱們發現每一個線程所產生的序號雖然都共享同一個TestNum實例,但它們並無發生相互干擾的狀況,而是各自產生獨立的序列號,這是由於咱們經過ThreadLocal爲每個線程提供了單獨的副本。

4、ThreadLocal的應用場景

最多見的ThreadLocal使用場景爲 用來解決 數據庫鏈接、Session管理等。

如:

private static ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
    public Connection initialValue() {
        return DriverManager.getConnection(DB_URL);
    }
};
 
public static Connection getConnection() {
    return connectionHolder.get();
}
private static final ThreadLocal threadSession = new ThreadLocal();
 
public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if (s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}

    ThreadLocal對象一般用於防止對可變的單實例變量或全局變量進行共享

  當一個類中使用了static成員變量的時候,必定要多問問本身,這個static成員變量須要考慮線程安全嗎?也就是說,多個線程須要獨享本身的static成員變量嗎?若是須要考慮,不妨使用ThreadLocal。

  ThreadLocal的主要應用場景爲多線程多實例(每一個線程對應一個實例)的對象的訪問,而且這個對象不少地方都要用到。例如:同一個網站登陸用戶,每一個用戶服務器會爲其開一個線程,每一個線程中建立一個ThreadLocal,裏面存用戶基本信息等,在不少頁面跳轉時,會顯示用戶信息或者獲得用戶的一些信息等頻繁操做,這樣多線程之間並無聯繫並且當前線程也能夠及時獲取想要的數據。


      ThreadLocal一般用來共享數據,當你想在多個方法中使用某個變量,這個變量是當前線程的狀態,其它線程不依賴這個變量,你第一時間想到的就是把變量定義在方法內部,而後再方法之間傳遞參數來使用,這個方法能解決問題,可是有個煩人的地方就是,每一個方法都須要聲明形參,多處聲明,多處調用。影響代碼的美觀和維護。有沒有一種方法能將變量像private static形式來訪問呢?這樣在類的任何一處地方就都能使用。這個時候ThreadLocal大顯身手了。

5、總結

一、ThreadLocal 的實現思想,咱們在前面已經說了,每一個線程維護一個 ThreadLocalMap 的映射表,映射表的 key 是 ThreadLocal 實例自己,value 是要存儲的副本變量。ThreadLocal 實例自己並不存儲值,它只是提供一個在當前線程中找到副本值的 key。 以下圖所示:

二、線程隔離的祕密,就在於ThreadLocalMap這個類。ThreadLocalMap是ThreadLocal類的一個靜態內部類,它實現了鍵值對的設置和獲取(對比Map對象來理解),每一個線程中都有一個獨立的ThreadLocalMap副本,它所存儲的值,只能被當前線程讀取和修改。ThreadLocal類經過操做每個線程特有的ThreadLocalMap副本,從而實現了變量訪問在不一樣線程中的隔離。由於每一個線程的變量都是本身特有的,徹底不會有併發錯誤。還有一點就是,ThreadLocalMap存儲的鍵值對中的鍵是this對象指向的ThreadLocal對象,而值就是你所設置的對象了。

三、ThreadLocalMap並非爲了解決線程安全問題,而是提供了一種將實例綁定到當前線程的機制,相似於隔離的效果,實際上本身在方法中new出來變量也能達到相似的效果。ThreadLocalMap跟線程安全基本不搭邊,綁定上去的實例也不是多線程公用的,而是每一個線程new一份,這個實例確定不是共用的,若是共用了,那就會引起線程安全問題。ThreadLocalMap最大的用處就是用來把實例變量共享成全局變量,在程序的任何方法中均可以訪問到該實例變量而已。網上不少人說ThreadLocalMap是解決了線程安全問題,實際上是望文生義,二者不是同類問題。

四、ThreadLocal設計的初衷是爲了解決多線程編程中的資源共享問題。提起這個,你們通常會想到synchronized,synchronized採起的是「以時間換空間」的策略,本質上是對關鍵資源上鎖,讓你們排隊操做。而ThreadLocal採起的是「以空間換時間」的思路,爲每一個使用該變量的線程提供獨立的變量副本,在本線程內部,它至關於一個「全局變量」,能夠保證本線程任什麼時候間操縱的都是同一個對象。

五、ThreadLocal類最重要的一個概念是,其原理是經過一個ThreadLocal的靜態內部類ThreadLocalMap實現,可是實際中,ThreadLocal不保存ThreadLocalMap,而是有每一個Thread內部維護ThreadLocal.ThreadLocalMap threadLocals一份數據結構。

這裏畫張圖更容易理解,假如咱們有以下的代碼:

class ThreadLocalDemo
{
    ThreadLocal<Integer> localA = new ThreadLocal<Integer>();
    ThreadLocal<Integer> localB = new ThreadLocal<Integer>();
}

在多線程環境下,數據結構應該是以下圖所示:

 六、ThreadLocal使用的通常步驟:

(1)在多線程的類(如ThreadDemo類)中,建立一個ThreadLocal對象threadXxx,用來保存線程間須要隔離處理的對象xxx。  

(2)在ThreadDemo類中,建立一個獲取要隔離訪問的數據的方法getXxx(),在方法中判斷,若ThreadLocal對象爲null時候,應該new()一個隔離訪問類型的對象,並強制轉換爲要應用的類型。  

(3)在ThreadDemo類的run()方法中,經過getXxx()方法獲取要操做的數據,這樣能夠保證每一個線程對應一個數據對象,在任什麼時候刻都操做的是這個對象。

七、ThreadLocal 與 synchronized 的對比

(1)ThreadLocal和synchonized都用於解決多線程併發訪問。可是ThreadLocal與synchronized有本質的區別。synchronized是利用鎖的機制,使變量或代碼塊在某一時該只能被一個線程訪問。而ThreadLocal爲每個線程都提供了變量的副本,使得每一個線程在某一時間訪問到的並非同一個對象,這樣就隔離了多個線程對數據的數據共享。而synchronized卻正好相反,它用於在多個線程間通訊時可以得到數據共享。

(2)synchronized用於線程間的數據共享,而ThreadLocal則用於線程間的數據隔離。

八、一句話理解ThreadLocal:向ThreadLocal裏面存東西就是向它裏面的Map存東西的,而後ThreadLocal把這個Map掛到當前的線程底下,這樣Map就只屬於這個線程了。

相關文章
相關標籤/搜索