一陆爽、簡介
對于ThreadLocal的簡介躺酒,我們先來看一下API文檔:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
??ThreadLocal眷篇,被稱為線程本地變量或線程私有變量抹缕,這種變量在多線程訪問下的時候能夠保證各個線程里的變量相對獨立于其他線程內(nèi)的變量揣非,這樣使得線程只有自己能訪問和修改對應的變量叛氨,從而可以實現(xiàn)多個線程間變量的隔離,從而實現(xiàn)變量的線程安全擂橘。通常情況下晌区,ThreadLocal變量類型都是private static類型的,用于關(guān)聯(lián)線程和線程的上下文通贞。
參考別人總結(jié)的一句話:
ThreadLocal的作用是提供線程內(nèi)的局部變量契讲,這種變量在線程的生命周期內(nèi)起作用,減少同一個線程內(nèi)多個函數(shù)或者組件之間一些公共變量的傳遞的復雜度滑频。
二捡偏、ThreadLocal
1. 實例
我們先看一個簡單的例子:
public class ThreadTest {
public static class MyRunnable extends Thread {
private static Integer random;
@Override
public void run() {
random = (int) (Math.random() * 1000);
System.out.println("線程" + Thread.currentThread().getName() + "獲取random:" + random);
}
}
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
new Thread(runnable).start();
new Thread(runnable).start();
}
}
由于我們沒有進行相應的線程同步的操作,所以多線程直接操作的話峡迷,很大程序上會出現(xiàn)第二個線程的值覆蓋掉第一個線程的值银伟,最終兩個值是相同的。
2. ThreadLocal源碼
我們會來簡單看下ThreadLocal的源碼绘搞,ThreadLocal提供的public的方法不多:
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)
public T get()
public void set(T value)
public void remove()
-
get
方法是用來獲取當前線程在ThreadLocal中保存的變量的副本彤避; -
set
方法是將當前線程對應的變量保存一份到ThreadLocal中; -
remove
方法用于移除當前線程在ThreadLocal中的副本夯辖; -
withInitial
方法用于初始化一個默認值的ThreadLocal琉预;
接下來,我們主要來看一下get方法:
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方法所操作的其實是一個ThreadLocalMap蒿褂,我們來簡單看下這個Map的結(jié)構(gòu):
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
??可以看到圆米,我們最終保存,獲取線程本地變量所操作的對象其實是WeakReference啄栓,也就是弱引用娄帖,弱引用我們前面已經(jīng)了解過,這里使用弱引用昙楚,是為了更好的垃圾回收近速,因為弱引用的生命周期是兩次GC之間。其實ThreadLocalMap的定義和WeakHashMap有點相似,感興趣的可以查看下以前學習WeakHashMap時的內(nèi)容:Java1.8-WeakHashMap源碼解析
我們先順著源碼來看下get方法的流程削葱,首先獲取當前線程奖亚,然后把當前線程對象作為key獲取ThreadLocalMap:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
這里獲取到的ThreadLocalMap其實是Thread的內(nèi)部變量,每個線程內(nèi)部維護了一個ThreadLocalMap析砸,用于存儲與此線程相關(guān)的ThreadLocal值:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
然后獲取該線程對應的ThreadLocalMap中的Entry遂蛀,如果不為空,獲取Entry的value屬性的值干厚;
- 如果map為空,或者map中對應的Entry為空螃宙,則調(diào)用setInitialValue初始化并返回相應的值蛮瞄;
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
到這里我們來簡單總結(jié)下ThreadLocal是如何為每個線程創(chuàng)建變量副本的:
- 首先,每個線程內(nèi)部都有一個變量
threadLocals
谆扎,該變量就是用來保存相應的變量副本的挂捅,key為ThreadLocal對象,value是相應的變量值堂湖;- 初始化該變量的的時候闲先,無論是通過get或者set方法都會進行相應的判斷,如果為空的話无蜂,進行相應的初始化伺糠;
- get方法中的通過ThreadLocalMap獲取對應的Entry,是根據(jù)ThreadLocal作為參數(shù)斥季,這點也可以看出Thread中的變量
threadLocals
的存儲方式训桶;
到這里,get方法就介紹完成了酣倾,至于set舵揭,remove方法的實現(xiàn)其實和get方法類似,就不多說了躁锡,貼上源碼簡單了解下:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
另外午绳,針對最開始的隨機數(shù)問題,我們可以修改為如下代碼:
public static class MyRunnable extends Thread {
private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
@Override
public void run() {
threadLocal.set( (int) (Math.random() * 100D) );
System.out.println("線程" + Thread.currentThread().getName() + "獲取random:" + threadLocal.get());
}
}
3. InheritableThreadLocal
??InheritableThreadLocal 是ThreadLocal的子類映之,由于ThreadLocal內(nèi)部每個線程都只能訪問到自己的私有變量值拦焚,而該繼承類的作用是允許一個線程創(chuàng)建的所有子線程可以訪問其父線程的值。我們來簡單看下兩個例子即可杠输,代碼來自:java并發(fā)編程學習: ThreadLocal使用及原理
public class ThreadTest {
public static class MyRunnable implements Runnable {
private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public MyRunnable(){
threadLocal.set((int) (Math.random() * 100D));
System.out.println("構(gòu)造方法:" + Thread.currentThread().getName() + ":" + threadLocal.get());
}
@Override
public void run() {
System.out.println("run方法:" + Thread.currentThread().getName() + ":" + threadLocal.get());
}
}
public static void main(String[] args) {
new Thread(new MyRunnable(), "A").start();
new Thread(new MyRunnable(), "B").start();
}
}
先看下打印結(jié)果:
構(gòu)造方法:main:92
構(gòu)造方法:main:43
run方法:A:null
run方法:B:null
??這里耕漱,我們把ThreadLocal賦值的地方放到了構(gòu)造方法中,然后在run方法中獲取該值抬伺,但卻沒有獲取到螟够。
這是因為new MyRunnable構(gòu)造方法是由main線程調(diào)用的,所以ThreadLocal的set方法,實際上是對main線程進行操作的妓笙,因此也只能在main線程中進行獲取若河,而run方法的上下文環(huán)境是子線程,所以自然獲取不到寞宫。接下來我們將ThreadLocal修改為
InheritableThreadLocal
萧福,然后再來看一下結(jié)果:
構(gòu)造方法:main:53
構(gòu)造方法:main:29
run方法:A:53
run方法:B:29
可以看到,在run方法中調(diào)用獲取到了父線程的值辈赋。InheritableThreadLocal的實現(xiàn)和ThreadLocal類似鲫忍,在Thread類中也保存了一個該類型的變量:
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
三、ThreadLocal的內(nèi)存泄漏問題
1. 為什么會發(fā)生內(nèi)存泄漏钥屈?
??由于ThreadLocalMap的key使用的是弱引用類型悟民,由于弱引用的生命周期比較短,存在于兩次GC之間的這段時間篷就,那么如果key在GC的時候被回收了射亏,那么就會導致value永遠不會被調(diào)用到,但如果對應的線程一直不結(jié)束竭业,那value就一直存在智润,這樣的話,就會有可能出現(xiàn)內(nèi)存泄漏的情況未辆。
2. 如何避免內(nèi)存泄漏
JDK的開發(fā)者為了解決這些潛在的問題窟绷,其實已經(jīng)做了一些改進,在ThreadLocal的get咐柜,set钾麸,remove方法都有相應的處理。首先炕桨,在ThreadLocalMap的定義上饭尝,有如下注釋:
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
??意思是說,當該map中的key==null的時候献宫,就可以認為這個值無效了钥平,這個無效的key被稱為stale entry
,就可以調(diào)用expunged進行刪除姊途,而expunged所對應的方法expungeStaleEntry
方法在set涉瘾,get及remove方法都有直接或間接的調(diào)用,所以有的文章中說捷兰,線程調(diào)用完成之后立叛,調(diào)用remove方法來避免內(nèi)存泄露,因為remove方法直接調(diào)用了該方法來刪除相應的value贡茅。
有關(guān)清除value源碼方面的解析可以參考這篇文章:從源碼角度深入詳解ThreadLocal內(nèi)存泄漏問題
3. 總結(jié)
- 首先我們要明白秘蛇,即使是key被回收了value還存在其做,是不一定會發(fā)生內(nèi)存泄漏的。首先赁还,一般情況下妖泄,程序都會調(diào)用
expungeStaleEntry
方法來進行清理,然后艘策,即使不調(diào)用該方法清理蹈胡,線程一旦消亡了,那對應的ThreadLocal朋蔫,ThreadLocalMap及對應的entry 自然也會被GC給回收掉了罚渐。- 當然如果線程一直不消亡,還是會有內(nèi)存泄漏的可能的驯妄,而常用的線程一直不結(jié)束的場景荷并,就是線程池了,因為這種情況下富玷,線程是一種重復利用的,有可能會造成value一直累加的情況既穆,具體的模擬可以參考: 深入理解ThreadLocal的"內(nèi)存溢出"
- 綜上赎懦,線程會發(fā)生內(nèi)存泄漏的情況一般是使用線程之后,我們沒有進行及時清理幻工;另一種情況就是線程池的使用励两;所以,無論哪種情況囊颅,在使用完成之后一定要記得及時清理当悔;
- JDK建議將ThreadLocal變量定義為private static的,這樣的話ThreadLocal的生命周期就會變長踢代,由于一直存在ThreadLocal的強引用盲憎,所以ThreadLocal也就不會被回收,這樣的話能保證任何時候都可以根據(jù)ThreadLocal的弱引用訪問到Entry的value值胳挎,然后remove掉饼疙,防止內(nèi)存泄漏。
四慕爬、應用場景
在常用的框架中窑眯,有許多使用到ThreadLocal的地方,比如Spring的事務(wù):
public abstract class TransactionSynchronizationManager {
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<Map<Object, Object>>("Transactional resources");
//事務(wù)注冊的事務(wù)同步器
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<Set<TransactionSynchronization>>("Transaction synchronizations");
//事務(wù)名稱
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<String>("Current transaction name");
//事務(wù)只讀屬性
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<Boolean>("Current transaction read-only status");
//事務(wù)隔離級別
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<Integer>("Current transaction isolation level");
//事務(wù)同步開啟
private static final ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<Boolean>("Actual transaction active");
}
- 我們可以使用ThreadLocal來記錄日志医窿,比如我們想記錄下多線程下程序的日志記錄磅甩,以方便錯誤情況下更容易的查到與某個特定線程相關(guān)的記錄。
- 我們還可以通過ThreadLocal姥卢,然后借助攔截器來記錄接口的調(diào)用時間卷要;
五渣聚、總結(jié)
- ThreadLocal的實現(xiàn)其實并不是特別復雜,首先却妨,ThreadLocal的內(nèi)部定義了一個ThreadLocalMap內(nèi)部類饵逐,用于變量的存儲。而每個線程內(nèi)部則有一個ThreadLocalMap的變量彪标,該變量的key是ThreadLocal倍权,value是對應的變量,這樣就實現(xiàn)了兩個線程同時訪問一個ThreadLocal變量捞烟,但兩個線程所讀到的變量都是各自線程獨有的薄声。也就是說ThreadLocal本身并不存儲,它只是作為一個key來讓線程從ThreadLocalMap來獲取值题画。
- ThreadLocalMap使用弱引用來作為key默辨,如果ThreadLocal沒有外部引用來引用它,那么GC的時候就會被回收掉苍息;不過使用ThreadLocal存在內(nèi)存泄漏的風險缩幸,所以每次使用完成之后,盡量都調(diào)用它的remove方法竞思,來清除數(shù)據(jù)表谊;
本文參考自:
海子-Java并發(fā)編程:深入剖析ThreadLocal
關(guān)于ThreadLocal內(nèi)存泄露的備忘
詳細介紹并發(fā)容器之ThreadLocal
知乎-深入剖析ThreadLocal
《并發(fā)編程實戰(zhàn)》