一、對ThreadLocal的理解
ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲,其實意思差不多。可能很多朋友都知道ThreadLocal為變量在每個線程中都創建了一個副本,那么每個線程可以訪問自己內部的副本變量。這句話從字面上看起來很容易理解,但是真正理解并不是那么容易。
ThreadLocal的官方API解釋為:
"該類提供了線程局部 (thread-local) 變量。這些變量不同于它們的普通對應物,因為訪問某個變量(通過其 get 或 set 方法)的每個線程都有自己的局部變量,它獨立于變量的初始化副本。ThreadLocal 實例通常是類中的 private static 字段,它們希望將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。"
大概的意思有兩點:
1、ThreadLocal提供了一種訪問某個變量的特殊方式:訪問到的變量屬于當前線程,即保證每個線程的變量不一樣,而同一個線程在任何地方拿到的變量都是一致的,這就是所謂的線程隔離。
2、如果要使用ThreadLocal,通常定義為private static類型,在我看來最好是定義為private static final類型。
很多博客都這樣說:ThreadLocal為解決多線程程序的并發問題提供了一種新的思路;ThreadLocal的目的是為了解決多線程訪問資源時的共享問題。如果你也這樣認為的,那現在給你10秒鐘,清空之前對ThreadLocal的錯誤的認知!
ThreadLocal可以總結為一句話:ThreadLocal的作用是提供線程內的局部變量,這種變量在線程的生命周期內起作用,減少同一個線程內多個函數或者組件之間一些公共變量的傳遞的復雜度。
我們還是先來看一個例子:
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要大。
二、深入解析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里面查找。
三、示例
示例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為每一個線程提供了單獨的副本。
四、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大顯身手了。
五、總結
1、ThreadLocal 的實現思想,我們在前面已經說了,每個線程維護一個 ThreadLocalMap 的映射表,映射表的 key 是 ThreadLocal 實例本身,value 是要存儲的副本變量。ThreadLocal 實例本身并不存儲值,它只是提供一個在當前線程中找到副本值的 key。 如下圖所示:
2、線程隔離的秘密,就在于ThreadLocalMap這個類。ThreadLocalMap是ThreadLocal類的一個靜態內部類,它實現了鍵值對的設置和獲取(對比Map對象來理解),每個線程中都有一個獨立的ThreadLocalMap副本,它所存儲的值,只能被當前線程讀取和修改。ThreadLocal類通過操作每一個線程特有的ThreadLocalMap副本,從而實現了變量訪問在不同線程中的隔離。因為每個線程的變量都是自己特有的,完全不會有并發錯誤。還有一點就是,ThreadLocalMap存儲的鍵值對中的鍵是this對象指向的ThreadLocal對象,而值就是你所設置的對象了。
3、ThreadLocalMap并不是為了解決線程安全問題,而是提供了一種將實例綁定到當前線程的機制,類似于隔離的效果,實際上自己在方法中new出來變量也能達到類似的效果。ThreadLocalMap跟線程安全基本不搭邊,綁定上去的實例也不是多線程公用的,而是每個線程new一份,這個實例肯定不是共用的,如果共用了,那就會引發線程安全問題。ThreadLocalMap最大的用處就是用來把實例變量共享成全局變量,在程序的任何方法中都可以訪問到該實例變量而已。網上很多人說ThreadLocalMap是解決了線程安全問題,其實是望文生義,兩者不是同類問題。
4、ThreadLocal設計的初衷是為了解決多線程編程中的資源共享問題。提起這個,大家一般會想到synchronized,synchronized采取的是“以時間換空間”的策略,本質上是對關鍵資源上鎖,讓大家排隊操作。而ThreadLocal采取的是“以空間換時間”的思路,為每個使用該變量的線程提供獨立的變量副本,在本線程內部,它相當于一個“全局變量”,可以保證本線程任何時間操縱的都是同一個對象。
5、ThreadLocal類最重要的一個概念是,其原理是通過一個ThreadLocal的靜態內部類ThreadLocalMap實現,但是實際中,ThreadLocal不保存ThreadLocalMap,而是有每個Thread內部維護ThreadLocal.ThreadLocalMap threadLocals
一份數據結構。
這里畫張圖更容易理解,假如我們有如下的代碼:
class ThreadLocalDemo { ThreadLocal<Integer> localA = new ThreadLocal<Integer>(); ThreadLocal<Integer> localB = new ThreadLocal<Integer>(); }
在多線程環境下,數據結構應該是如下圖所示:
6、ThreadLocal使用的一般步驟:
(1)在多線程的類(如ThreadDemo類)中,創建一個ThreadLocal對象threadXxx,用來保存線程間需要隔離處理的對象xxx。
(2)在ThreadDemo類中,創建一個獲取要隔離訪問的數據的方法getXxx(),在方法中判斷,若ThreadLocal對象為null時候,應該new()一個隔離訪問類型的對象,并強制轉換為要應用的類型。
(3)在ThreadDemo類的run()方法中,通過getXxx()方法獲取要操作的數據,這樣可以保證每個線程對應一個數據對象,在任何時刻都操作的是這個對象。
7、ThreadLocal 與 synchronized 的對比
(1)ThreadLocal和synchonized都用于解決多線程并發訪問。但是ThreadLocal與synchronized有本質的區別。synchronized是利用鎖的機制,使變量或代碼塊在某一時該只能被一個線程訪問。而ThreadLocal為每一個線程都提供了變量的副本,使得每個線程在某一時間訪問到的并不是同一個對象,這樣就隔離了多個線程對數據的數據共享。而synchronized卻正好相反,它用于在多個線程間通信時能夠獲得數據共享。
(2)synchronized用于線程間的數據共享,而ThreadLocal則用于線程間的數據隔離。
8、一句話理解ThreadLocal:向ThreadLocal里面存東西就是向它里面的Map存東西的,然后ThreadLocal把這個Map掛到當前的線程底下,這樣Map就只屬于這個線程了。
文章列表