精通高并發與多線程,卻不會用ThreadLocal?
本文轉載自微信公眾號「小菜良記」,作者小菜良記。轉載本文請聯系小菜良記公眾號。
ThreadLocal 簡介
概念ThreadLocal 類是用來提供線程內部的局部變量。這種變量在多線程環境下訪問(get 和set 方法訪問)時能保證各個線程的變量相對獨立于其他線程內的變量。ThreadLocal 實例通常來說都是 private static 類型的,用于關聯線程和上下文。
作用
- 傳遞數據
提供線程內部的局部變量??梢酝ㄟ^ ThreadLocal 在同一線程,不同組件中傳遞公共變量。
- 線程并發
適用于多線程并發情況下。
- 線程隔離
每個線程的變量都是獨立的,不會相互影響。
ThreadLocal 實戰
1. 常見方法
- ThreadLocal ()
構造方法,創建一個 ThreadLocal 對象
- void set (T value)
設置當前線程綁定的局部變量
- T get ()
獲取當前線程綁定的局部變量
- void remove ()
移除當前線程綁定的局部變量
2. 為什么要使用 ThreadLocal
首先我們先看一組并發條件下的代碼場景:
- @Data
- public class ThreadLocalTest {
- private String name;
- public static void main(String[] args) {
- ThreadLocalTest tmp = new ThreadLocalTest();
- for (int i = 0; i < 4; i++) {
- Thread thread = new Thread(() -> {
- tmp.setName(Thread.currentThread().getName());
- System.out.println(Thread.currentThread().getName() +
- "\t 拿到數據:" + tmp.getName());
- });
- thread.setName("Thread-" + i);
- thread.start();
- }
- }
- }
我們理想中的代碼輸出結果應該是這樣的:
- /** OUTPUT **/
- Thread-0 拿到數據:Thread-0
- Thread-1 拿到數據:Thread-1
- Thread-2 拿到數據:Thread-2
- Thread-3 拿到數據:Thread-3
但是實際上輸出的結果卻是這樣的:
- /** OUTPUT **/
- Thread-0 拿到數據:Thread-1
- Thread-3 拿到數據:Thread-3
- Thread-1 拿到數據:Thread-1
- Thread-2 拿到數據:Thread-2
順序亂了沒有關系,但是我們可以看到 Thread-0 這個線程拿到的值卻是 Thread-1
從結果中我們可以看出多個線程在訪問同一個變量的時候會出現異常,這是因為線程間的數據沒有隔離!
并發線程出現的問題?那加鎖不就完事了!這個時候你三下五除二的寫下了以下代碼:
- @Data
- public class ThreadLocalTest {
- private String name;
- public static void main(String[] args) {
- ThreadLocalTest tmp = new ThreadLocalTest();
- for (int i = 0; i < 4; i++) {
- Thread thread = new Thread(() -> {
- synchronized (tmp) {
- tmp.setName(Thread.currentThread().getName());
- System.out.println(Thread.currentThread().getName()
- + "\t" + tmp.getName());
- }
- });
- thread.setName("Thread-" + i);
- thread.start();
- }
- }
- }
- /** OUTPUT **/
- Thread-2 Thread-2
- Thread-3 Thread-3
- Thread-1 Thread-1
- Thread-0 Thread-0
從結果上看,加鎖好像是解決了上述問題,但是 synchronized 常用于多線程數據共享的問題,而非多線程數據隔離的問題。這里使用 synchronized 雖然解決了問題,但是多少有些不合適,并且 synchronized 屬于重量級鎖,為了實現多線程數據隔離貿然的加上synchronized,也會影響到性能。
加鎖的方法也被否定了,那么該如何解決?不如用 ThreadLocal 牛刀小試一番:
- public class ThreadLocalTest {
- private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
- public String getName() {
- return threadLocal.get();
- }
- public void setName(String name) {
- threadLocal.set(name);
- }
- public static void main(String[] args) {
- ThreadLocalTest tmp = new ThreadLocalTest();
- for (int i = 0; i < 4; i++) {
- Thread thread = new Thread(() -> {
- tmp.setName(Thread.currentThread().getName());
- System.out.println(Thread.currentThread().getName() +
- "\t 拿到數據:" + tmp.getName());
- });
- thread.setName("Thread-" + i);
- thread.start();
- }
- }
- }
在查看輸出結果之前,我們先來看看代碼發生了那些變化
首先多了一個 private static 修飾的 ThreadLocal ,然后在 setName 的時候,我們實際上是往 ThreadLocal 里面存數據,在 getName 的時候,我們是在 ThreadLocal 里面取數據。感覺操作上也是挺簡單的,但是這樣真的能做到線程間的數據隔離嗎,我們再來看一看結果:
- /** OUTPUT **/
- Thread-1 拿到數據:Thread-1
- Thread-2 拿到數據:Thread-2
- Thread-0 拿到數據:Thread-0
- Thread-3 拿到數據:Thread-3
從結果上可以看到每個線程都能取到對應的數據。ThreadLocal 也已經解決了多線程之間數據隔離的問題。
那么我們來小結一下,為什么需要使用ThreadLocal,與 synchronized 的區別是什么
- synchronized
原理: 同步機制采用 "以時間換空間" 的方式,只提供了一份變量,讓不同線程排隊訪問
側重點: 多個線程之間同步訪問資源
- ThreadLocal
原理: ThreadLocal 采用 "以空間換時間" 的方式,為每個線程都提供了一份變量的副本,從而實現同時訪問而互不干擾
側重點: 多線程中讓每個線程之間的數據相互隔離
3. 內部結構
從上面的案例中我們可以看到 ThreadLocal 的兩個主要方法分別是 set() 和 get()
那我們不妨猜想一下,如果讓我們來設計 ThreadLocal ,我們該如何設計,是否會有這樣的想法:每個 ThreadLocal 都創建一個 Map,然后用線程作為 Map 的 key,要存儲的局部變量作為 Map 的 value ,這樣就能達到各個線程的局部變量隔離的效果。
這個想法也是沒錯的,早期的 ThreadLocal 便是這樣設計的,但是在 JDK 8 之后便更改了設計,如下:
設計過程:
- 每個 Thread 線程內部都有一個 ThreadLocalMap
- ThreadLocalMap 中存儲著以 ThreadLocal 對象為 key ,線程變量為 value
- Thread 內部的 Map 是由 ThreadLocal 維護的,由 ThreadLocal 負責向 Map 設置和獲取線程的變量值
- 對于不同的線程,每次獲取副本值時,別的線程并不能獲取到線程的副本值,這樣就會形成副本的隔離,互不干擾
注: 每個線程都要有自己的一個 map,但是這個類就是一個普通的 Java 類,并沒有實現Map 接口,但是具有類似 Map 類似的功能。
通過這樣實現看起來貌似會比之前我們猜想的更加復雜,這樣做的好處是什么呢?
- 每個 Map 存儲的 Entry 數量就會變少,因為之前的存儲數量由 Thread 的數量決定,現在是由 ThreadMap 的數量決定,在實際開發中,ThreadLocal 的數量要更少于Thread 的數量。
- 當 Thread 銷毀之后,對應的 ThreadLocalMap 也會隨之銷毀,能減少內存的使用
4. 源碼分析
首先我們先看 ThreadLocalMap 中有哪些成員:
如果你看過 HashMap 的源碼,肯定會覺得這幾個特別熟悉,其中:
- INITIAL_CAPACITY:初始容量,必須是 2 的整次冪
- table:存放數據的table
- size:數組中 entries 的個數,用于判斷 table 當前使用量是否超過閾值
- threshold:進行擴容的閾值,表使用量大于它的時候會進行擴容
ThreadLocals
Thread 類中有個類型為 ThreadLocal.ThreadLocalMap 類型的變量 ThreadLocals ,這個就是用來保存每個線程的私有數據。
ThreadLocalMap
ThreadLocalMap是ThreadLocal的內部類,每個數據用Entry保存,其中的Entry用一個鍵值對存儲,鍵為ThreadLocal的引用。
我們可以看到 Entry 繼承于WeakReference,這是因為如果是強引用,即使把ThreadLocal 設置為 null,GC 也不會回收,因為 ThreadLocalMap 對它有強引用。
在沒有手動刪除這個Entry以及CurrentThread依然運行的前提下,始終有強引用鏈threadRef -> currentThread -> threadLocalMap -> entry,Entry就不會被回收(Entry中包括了ThreadLocal實例和value),導致Entry內存泄漏。
那是不是就是說如果使用了弱引用,就不會造成內存泄露 呢,這也是不正確的。
因為如果我們沒有手動刪除 Entry 的情況下,此時 Entry 中的 key == null,這個時候沒有任何強引用指向 threaLocal 實例,所以 threadLocal 就可以順利被 gc 回收,但是value 不會被回收,而這塊的 value 永遠不會被訪問到,因此會導致內存泄露
接下來我們看下 ThreadLocalMap 的幾個核心方法:
set 方法
首先我們先看下源碼:
- public void set(T value) {
- // 獲取當前線程對象
- Thread t = Thread.currentThread();
- // 獲取此線程對象中維護的ThreadLocalMap對象
- ThreadLocalMap map = getMap(t);
- // 判斷map是否存在
- if (map != null)
- // 存在則調用map.set設置此實體entry
- map.set(this, value);
- else
- // 如果當前線程不存在ThreadLocalMap對象則調用createMap進行ThreadLocalMap對象的初始化
- // 并將 t(當前線程)和value(t對應的值)作為第一個entry存放至ThreadLocalMap中
- createMap(t, value);
- }
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
- void createMap(Thread t, T firstValue) {
- //這里的this是調用此方法的threadLocal
- t.threadLocals = new ThreadLocalMap(this, firstValue);
- }
執行流程:
- 首先獲取當前線程,并根據當前線程獲取一個 map
- 如果獲取的 map 不為空,則將參數設置到 map 中(當前 ThreadLocal 的引用作為key )
- 如果 Map 為空,則給該線程創建 map ,并設置初始值
get 方法
源碼如下:
- public T get() {
- // 獲取當前線程對象
- Thread t = Thread.currentThread();
- // 獲取此線程對象中維護的ThreadLocalMap對象
- ThreadLocalMap map = getMap(t);
- // 如果此map存在
- if (map != null) {
- // 以當前的ThreadLocal 為 key,調用getEntry獲取對應的存儲實體e
- ThreadLocalMap.Entry e = map.getEntry(this);
- // 對e進行判空
- if (e != null) {
- @SuppressWarnings("unchecked")
- // 獲取存儲實體 e 對應的 value值
- // 即為我們想要的當前線程對應此ThreadLocal的值
- T result = (T)e.value;
- return result;
- }
- }
- return setInitialValue();
- }
- private T setInitialValue() {
- // 調用initialValue獲取初始化的值
- // 此方法可以被子類重寫, 如果不重寫默認返回null
- T value = initialValue();
- // 獲取當前線程對象
- Thread t = Thread.currentThread();
- // 獲取此線程對象中維護的ThreadLocalMap對象
- ThreadLocalMap map = getMap(t);
- // 判斷map是否存在
- if (map != null)
- // 存在則調用map.set設置此實體entry
- map.set(this, value);
- else
- // 如果當前線程不存在ThreadLocalMap對象則調用createMap進行ThreadLocalMap對象的初始化
- // 并將 t(當前線程)和value(t對應的值)作為第一個entry存放至ThreadLocalMap中
- createMap(t, value);
- // 返回設置的值value
- return value;
- }
執行流程:
- 首先獲取當前線程,根據當前線程獲取一個 map
- 如果獲取的 map 不為空,則在 map 中以 ThreadLocal 的引用作為 key 來在 map 中獲取對應的 Entry entry ,否則跳轉到第四步
- 如果 Entry entry 不為空 ,則返回 entry.value ,否則跳轉到第四步
- map 為空或者 entry 為空,則通過 initialValue 函數獲取初始值 value ,然后用ThreadLocal 的引用和 value 作為 firstKey 和 firstValue 創建一個新的 map
remove 方法
源碼如下:
- public void remove() {
- // 獲取當前線程對象中維護的ThreadLocalMap對象
- ThreadLocalMap m = getMap(Thread.currentThread());
- // 如果此map存在
- if (m != null)
- // 存在則調用map.remove
- m.remove(this);
- }
- // 以當前ThreadLocal為key刪除對應的實體entry
- private void remove(ThreadLocal<?> key) {
- Entry[] tab = table;
- int len = tab.length;
- int i = key.threadLocalHashCode & (len-1);
- for (Entry e = tab[i];
- e != null;
- e = tab[i = nextIndex(i, len)]) {
- if (e.get() == key) {
- e.clear();
- expungeStaleEntry(i);
- return;
- }
- }
- }
執行流程:
首先獲取當前線程,并根據當前線程獲取一個 map
如果獲得的map 不為空,則移除當前 ThreadLocal 對象對應的 entry
initialValue 方法
源碼如下:
- protected T initialValue() {
- return null;
- }
在源碼中我們可以看到這個方法僅僅簡單的返回了 null ,這個方法是在線程第一次通過get () 方法訪問該線程的 ThreadLocal 時調用的,只有在線程先調用了 set () 方法才不會調用 initialValue () 方法,通常情況下,這個方法最多被調用一次。
如果們想要 ThreadLocal 線程局部變量有一個除 null 以外的初始值,那么就必須通過子類繼承 ThreadLocal 來重寫此方法,可以通過匿名內部類實現。