深入理解Java并發(fā)編程之ThreadLocal
ThreadLocal簡介
變量值的共享可以使用public static的形式,所有線程都使用同一個變量,如果想實現(xiàn)每一個線程都有自己的共享變量該如何實現(xiàn)呢?JDK中的ThreadLocal類正是為了解決這樣的問題。
ThreadLocal類并不是用來解決多線程環(huán)境下的共享變量問題,而是用來提供線程內部的共享變量,在多線程環(huán)境下,可以保證各個線程之間的變量互相隔離、相互獨立。在線程中,可以通過get()/set()方法來訪問變量。ThreadLocal實例通常來說都是private static類型的,它們希望將狀態(tài)與線程進行關聯(lián)。這種變量在線程的生命周期內起作用,可以減少同一個線程內多個函數(shù)或者組件之間一些公共變量的傳遞的復雜度。
我們先通過一個例子來看一下ThreadLocal的基本用法:
public class ThreadLocalTest { static class MyThread extends Thread { private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); @Override public void run() { super.run(); for (int i = 0; i < 3; i++) { threadLocal.set(i); System.out.println(getName() + " threadLocal.get() = " + threadLocal.get()); } } } public static void main(String[] args) { MyThread myThreadA = new MyThread(); myThreadA.setName("ThreadA"); MyThread myThreadB = new MyThread(); myThreadB.setName("ThreadB"); myThreadA.start(); myThreadB.start(); } }
運行結果(不唯一):
ThreadA threadLocal.get() = 0
ThreadB threadLocal.get() = 0
ThreadA threadLocal.get() = 1
ThreadA threadLocal.get() = 2
ThreadB threadLocal.get() = 1
ThreadB threadLocal.get() = 2
雖然兩個線程都在向threadLocal對象中set()數(shù)據(jù)值,但每個線程都還是能取出自己設置的數(shù)據(jù),確實可以達到隔離線程變量的效果。
ThreadLocal源碼解析
ThreadLocal常用方法介紹
- get()方法:獲取與當前線程關聯(lián)的ThreadLocal值。
- set(T value)方法:設置與當前線程關聯(lián)的ThreadLocal值。
- initialValue()方法:設置與當前線程關聯(lián)的ThreadLocal初始值。
當調用get()方法的時候,若是與當前線程關聯(lián)的ThreadLocal值已經被設置過,則不會調用initialValue()方法;否則,會調用initialValue()方法來進行初始值的設置。通常initialValue()方法只會被調用一次,除非調用了remove()方法之后又調用get()方法,此時,與當前線程關聯(lián)的ThreadLocal值處于沒有設置過的狀態(tài)(其狀態(tài)體現(xiàn)在源碼中,就是線程的ThreadLocalMap對象是否為null),initialValue()方法仍會被調用。
initialValue()方法是protected類型的,很顯然是建議在子類重載該函數(shù)的,所以通常該方法都會以匿名內部類的形式被重載,以指定初始值,例如:
public class ThreadLocalTest { public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return Integer.valueOf(1); } }; }
remove()方法:將與當前線程關聯(lián)的ThreadLocal值刪除。
實現(xiàn)原理
ThreadLocal最簡單的實現(xiàn)方式就是ThreadLocal類內部有一個線程安全的Map,然后用線程的ID作為Map的key,實例對象作為Map的value,這樣就能達到各個線程的值隔離的效果。
JDK最早期的ThreadLocal就是這樣設計的,但是,之后ThreadLocal的設計換了一種方式,我們先看get()方法的源碼,然后進一步介紹ThreadLocal的實現(xiàn)方式:
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
get()方法主要做了以下事情:
1、調用Thread.currentThread()獲取當前線程對象t;
2、根據(jù)當前線程對象,調用getMap(Thread)獲取線程對應的ThreadLocalMap對象:
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
threadLocals是Thread類的成員變量,初始化為null:
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
3、如果獲取的map不為空,則在map中以ThreadLocal的引用作為key來在map中獲取對應的value e,否則轉到步驟5;
4、若e不為null,則返回e中存儲的value值,否則轉到步驟5;
5、調用setInitialValue()方法,對線程的ThreadLocalMap對象進行初始化操作,ThreadLocalMap對象的key為ThreadLocal對象,value為initialValue()方法的返回值。
從上面的分析中,可以看到,ThreadLocal的實現(xiàn)離不開ThreadLocalMap類,ThreadLocalMap類是ThreadLocal的靜態(tài)內部類。每個Thread維護一個ThreadLocalMap映射表,這個映射表的key是ThreadLocal實例本身,value是真正需要存儲的Object。這樣的設計主要有以下幾點優(yōu)勢:
- 這樣設計之后每個Map的Entry數(shù)量變小了:之前是Thread的數(shù)量,現(xiàn)在是ThreadLocal的數(shù)量,能提高性能;
- 當Thread銷毀之后對應的ThreadLocalMap也就隨之銷毀了,能減少內存使用量。
ThreadLocalMap源碼分析
ThreadLocalMap是用來存儲與線程關聯(lián)的value的哈希表,它具有HashMap的部分特性,比如容量、擴容閾值等,它內部通過Entry類來存儲key和value,Entry類的定義為:
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
Entry繼承自WeakReference,通過上述源碼super(k);可以知道,ThreadLocalMap是使用ThreadLocal的弱引用作為Key的。
分析到這里,我們可以得到下面這個對象之間的引用結構圖(其中,實線為強引用,虛線為弱引用):
我們知道,弱引用對象在Java虛擬機進行垃圾回收時,就會被釋放,那我們考慮這樣一個問題:
ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部關聯(lián)的強引用,那么在虛擬機進行垃圾回收時,這個ThreadLocal會被回收,這樣,ThreadLocalMap中就會出現(xiàn)key為null的Entry,這些key對應的value也就再無妨訪問,但是value卻存在一條從Current Thread過來的強引用鏈。因此只有當Current Thread銷毀時,value才能得到釋放。
該強引用鏈如下:
CurrentThread Ref -> Thread -> ThreadLocalMap -> Entry -> value
因此,只要這個線程對象被gc回收,那些key為null對應的value也會被回收,這樣也沒什么問題,但在線程對象不被回收的情況下,比如使用線程池的時候,核心線程是一直在運行的,線程對象不會回收,若是在這樣的線程中存在上述現(xiàn)象,就可能出現(xiàn)內存泄露的問題。
那在ThreadLocalMap中是如何解決這個問題的呢?
在獲取key對應的value時,會調用ThreadLocalMap的getEntry(ThreadLocal<?> key)方法,該方法源碼如下:
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
通過key.threadLocalHashCode & (table.length - 1)來計算存儲key的Entry的索引位置,然后判斷對應的key是否存在,若存在,則返回其對應的value,否則,調用getEntryAfterMiss(ThreadLocal<?>, int, Entry)方法,源碼如下:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
ThreadLocalMap采用線性探查的方式來處理哈希沖突,所以會有一個while循環(huán)去查找對應的key,在查找過程中,若發(fā)現(xiàn)key為null,即通過弱引用的key被回收了,會調用expungeStaleEntry(int)方法,其源碼如下:
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
通過上述代碼可以發(fā)現(xiàn),若key為null,則該方法通過下述代碼來清理與key對應的value以及Entry:
// expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null;
此時,CurrentThread Ref不存在一條到Entry對象的強引用鏈,Entry到value對象也不存在強引用,那在程序運行期間,它們自然也就會被回收。expungeStaleEntry(int)方法的后續(xù)代碼就是以線性探查的方式,調整后續(xù)Entry的位置,同時檢查key的有效性。
在ThreadLocalMap中的set()/getEntry()方法中,都會調用expungeStaleEntry(int)方法,但是如果我們既不需要添加value,也不需要獲取value,那還是有可能產生內存泄漏的。所以很多情況下需要使用者手動調用ThreadLocal的remove()函數(shù),手動刪除不再需要的ThreadLocal,防止內存泄露。若對應的key存在,remove()方法也會調用expungeStaleEntry(int)方法,來刪除對應的Entry和value。
其實,最好的方式就是將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命周期就更長,由于一直存在ThreadLocal的強引用,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據(jù)ThreadLocal的弱引用訪問到Entry的value值,然后remove它,可以防止內存泄露。
InheritableThreadLocal
InheritableThreadLocal繼承自ThreadLocal,使用InheritableThreadLocal類可以使子線程繼承父線程的值,來看一段示例代碼:
public class ThreadLocalTest { private static InheritableThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>() { @Override protected Integer initialValue() { return Integer.valueOf(10); } }; static class MyThread extends Thread { @Override public void run() { super.run(); System.out.println(getName() + " inheritableThreadLocal.get() = " + inheritableThreadLocal.get()); } } public static void main(String[] args) { System.out.println(Thread.currentThread().getName() + " inheritableThreadLocal.get() = " + inheritableThreadLocal.get()); MyThread myThread = new MyThread(); myThread.setName("線程A"); myThread.start(); } }
運行結果:
main inheritableThreadLocal.get() = 10
線程A inheritableThreadLocal.get() = 10
可以看到子線程成功繼承了父線程的值。
父線程還可以設置子線程的初始值,只需要重寫InheritableThreadLocal類的childValue(T)方法即可,將上述代碼的inheritableThreadLocal 定義修改為如下方式:
private static InheritableThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>() { @Override protected Integer initialValue() { return Integer.valueOf(10); } @Override protected Integer childValue(Integer parentValue) { return Integer.valueOf(5); } };
運行結果為:
main inheritableThreadLocal.get() = 10
線程A inheritableThreadLocal.get() = 5
可以看到,子進程成功獲取到了父進程設置的初始值。
使用InheritableThreadLocal類需要注意的一點是,如果子線程在取得值的同時,主線程將InheritableThreadLocal中的值進行更改,那子線程獲取的還是舊值。
線程中用來實現(xiàn)上述功能的ThreadLocalMap類變量為
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
InheritableThreadLocal類的實現(xiàn)很簡單,主要是重寫了ThreadLocal類的getMap(Thread)方法和createMap(Thread, T)方法,將其中操作的ThreadLocalMap變量修改為了inheritableThreadLocals,這里不再進一步敘述。
參考資料
高洪巖:《Java多線程編程核心技術》
ThreadLocal和synchronized的區(qū)別
到此這篇關于深入理解Java并發(fā)編程之ThreadLocal 的文章就介紹到這了,更多相關Java ThreadLocal 內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java.lang.Long.parseLong()方法詳解及示例
這個java.lang.Long.parseLong(String s) 方法解析字符串參數(shù)s作為有符號十進制長,下面這篇文章主要給大家介紹了關于Java.lang.Long.parseLong()方法詳解及示例的相關資料,需要的朋友可以參考下2023-01-01Java使用Arrays.asList報UnsupportedOperationException的解決
這篇文章主要介紹了Java使用Arrays.asList報UnsupportedOperationException的解決,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-04-04Java中easypoi導入excel文件列名相同的處理方案
這篇文章主要介紹了Java中easypoi導入excel文件列名相同的處理方案,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-06-06Springboot結合JDBC實現(xiàn)雙數(shù)據(jù)源實例
這篇文章主要為大家介紹了Springboot結合JDBC實現(xiàn)雙數(shù)據(jù)源實例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-12-12淺談@RequestParam(required = true)的誤區(qū)
這篇文章主要介紹了@RequestParam(required = true)的誤區(qū),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11