前言
之前遇到的一個(gè)筆試題:簡(jiǎn)單介紹threadLocal夭咬,并解釋為什么其不會(huì)造成內(nèi)存泄漏机打。因?yàn)閷?duì)threadLocal不是很了解冒滩,所以借此機(jī)會(huì)了解一下
出現(xiàn)原因
通常情況下溪王,任何一個(gè)線程都可以訪問和修改我們所創(chuàng)建的變量毕箍,由此增加了線程間共享變量的風(fēng)險(xiǎn)弛房。
有時(shí)可能要避免共享變量,使用ThreadLocal輔助類為各個(gè)線程提供各自的實(shí)例
例如:
SimpleDateFormat
類不是線程安全的而柑。假設(shè)有一個(gè)靜態(tài)變量
public static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
如果有兩個(gè)線程都執(zhí)行如下操作:
String dateStamp = dateFormat.format(new Date());
結(jié)果可能很混亂文捶,因?yàn)閐ateFormat使用的內(nèi)部數(shù)據(jù)結(jié)構(gòu)可能會(huì)被并發(fā)的訪問
所破壞
那么針對(duì)上述,可以選擇的解決方案
- 同步——開銷太大
- 在需要時(shí)構(gòu)造一個(gè)局部SimpleDateFormat對(duì)象—— 太浪費(fèi)了
threadLocal介紹
可以實(shí)現(xiàn)每一個(gè)線程都有自己的專屬本地變量
ThreadLocal 類主要解決的就是讓每個(gè)線程綁定自己的值媒咳,可以將 ThreadLocal 類形象的比喻成存放數(shù)據(jù)的盒子粹排,盒子中可以存儲(chǔ)每個(gè)線程的私有數(shù)據(jù)
還是以上面的SimpleDateFormat
為例,要為每個(gè)線程構(gòu)造一個(gè)實(shí)例代碼:
public static final ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
要訪問具體的方法涩澡,可以調(diào)用:
String dateStamp = dateFormat.get().format(new Date());
在一個(gè)給定線程中顽耳,首次調(diào)用get()方法時(shí),會(huì)調(diào)用構(gòu)造器中的lambda表達(dá)式妙同,在此之后射富,就會(huì)直接返回屬于當(dāng)前線程的實(shí)例。
常用方法
java.lang.ThreadLoacl<T>
1.2
-
T get()
- 得到這個(gè)線程的當(dāng)前值渐溶。如果首次調(diào)用get辉浦,會(huì)調(diào)用initialize來(lái)得到這個(gè)值
-
void set(T t)
- 為這個(gè)線程設(shè)置一個(gè)新值
-
void remove()
- 刪除對(duì)應(yīng)這個(gè)線程的值
-
static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)
8- 創(chuàng)建一個(gè)線程局部變量弄抬,其初始值通過調(diào)用給定的提供者(supplier)生成
threadLocal源碼了解
Thread類中的有關(guān)變量
threadLocal定義——線程的局部變量茎辐,可以先看看Thread類中與其相關(guān)的變量:
public class Thread implements Runnable {
......
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
......
}
從上面 Thread 類 源代碼可以看出 Thread 類中有一個(gè) threadLocals 和 一個(gè) inheritableThreadLocals 變量,它們都是 ThreadLocalMap 類型的變量掂恕。
其中ThreadLocalMap可以理解為ThreadLocal 類實(shí)現(xiàn)的定制化的 HashMap
默認(rèn)情況下這兩個(gè)變量都是 null拖陆,只有當(dāng)前線程調(diào)用 ThreadLocal 類的 set 或 get 方法時(shí)才創(chuàng)建它們,實(shí)際上調(diào)用這兩 個(gè)方法的時(shí)候懊亡,我們調(diào)用的是 ThreadLocalMap 類對(duì)應(yīng)的 get() 依啰、 set() 方法
threadLocal的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();
ThreadLocalMap map = getMap(t);
// 若該線程的ThreadLocalMap對(duì)象已存在,則替換該Map里的值店枣;否則創(chuàng)建1個(gè)ThreadLocalMap對(duì)象
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
其中的getMap()方法返回的是類Thread中的threadLocals變量速警,代碼如下:
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
我們可以發(fā)現(xiàn)ThreadLocal類在調(diào)用set()方法時(shí)叹誉,最終的變量并不是保存在ThreadLocal上,而是保存在當(dāng)前線程的ThreadLocalMap 中闷旧。ThreadLocal 可以理解為只是 ThreadLocalMap 的封裝长豁,傳遞了變量值
所以,ThreadLocal 內(nèi)部維護(hù)的是一個(gè)類似 Map 的 ThreadLocalMap 數(shù)據(jù)結(jié)構(gòu)忙灼, key 為當(dāng)前對(duì)象的 Thread 對(duì)象匠襟,值為 Object 對(duì)象。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {...}
ThreadLocalMap 是 ThreadLocal 的靜態(tài)內(nèi)部類
ThreadLocal的get()方法
/**
* 獲取ThreadLocal變量里的值
* 由于ThreadLocal變量引用 指向 ThreadLocalMap對(duì)象该园,即獲取ThreadLocalMap對(duì)象的值 = 該線程設(shè)置的存儲(chǔ)在ThreadLocal變量的值
**/
public T get() {
// 1. 獲得當(dāng)前線程
Thread t = Thread.currentThread();
// 2. 獲取該線程的ThreadLocalMap對(duì)象
ThreadLocalMap map = getMap(t);
// 3. 若該線程的ThreadLocalMap對(duì)象已存在酸舍,則直接獲取該Map里的值;否則則通過初始化函數(shù)創(chuàng)建1個(gè)ThreadLocalMap對(duì)象
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value; // 直接獲取值
}
return setInitialValue(); // 初始化
}
/**
* 初始化ThreadLocal的值
**/
private T setInitialValue() {
T value = initialValue();
// 1. 獲得當(dāng)前線程
Thread t = Thread.currentThread();
// 2. 獲取該線程的ThreadLocalMap對(duì)象
ThreadLocalMap map = getMap(t);
// 3. 若該線程的ThreadLocalMap對(duì)象已存在里初,則直接替換該值啃勉;否則則創(chuàng)建
if (map != null)
map.set(this, value); // 替換
else
createMap(t, value); // 創(chuàng)建->>分析2
return value;
}
ThreadLocal如何做到線程安全
- 每個(gè)線程擁有自己獨(dú)立的ThreadLocals變量(指向ThreadLocalMap對(duì)象 )
- 每當(dāng)線程 訪問 ThreadLocals變量時(shí),訪問的都是各自線程自己的ThreadLocalMap變量(鍵 - 值)
- ThreadLocalMap變量的鍵 key = 唯一 = 當(dāng)前ThreadLocal實(shí)例
上述3點(diǎn) 保證了線程間的數(shù)據(jù)訪問隔離青瀑,即線程安全
ThreadLocal 內(nèi)存泄露問題
ThreadLocalMap中使用的key是弱引用璧亮,而value是強(qiáng)引用
Java中有四種引用類型:強(qiáng)引用娇妓、軟引用故慈、弱引用、虛引用
Java設(shè)計(jì)這四種引用的主要目的有兩個(gè):
- 可以讓程序員通過代碼的方式來(lái)決定某個(gè)對(duì)象的生命周期绰精;
- 有利于垃圾回收哑诊。
強(qiáng)引用:最普遍的一種引用群扶,我們寫的代碼,99.9999%都是強(qiáng)引用镀裤。只要某個(gè)對(duì)象有強(qiáng)引用與之關(guān)聯(lián)竞阐,這個(gè)對(duì)象永遠(yuǎn)不會(huì)被回收,即使內(nèi)存不足暑劝,JVM寧愿拋出OOM骆莹,也不會(huì)去回收
弱引用:當(dāng)內(nèi)存不足,會(huì)觸發(fā)JVM的GC担猛,如果GC后幕垦,內(nèi)存還是不足,就會(huì)把軟引用的包裹的對(duì)象給干掉傅联,也就是只有在內(nèi)存不足先改,JVM才會(huì)回收該對(duì)象
所以,如果ThreadLocal 沒有被外部強(qiáng)引用的情況下蒸走,在垃圾回收的時(shí)候仇奶,key 會(huì)被清理掉,而 value 不會(huì)被清理掉比驻。這樣一來(lái)该溯, ThreadLocalMap 中就會(huì)出現(xiàn) key 為 null 的 Entry岛抄。假如我們不做任何措施的話,value 永遠(yuǎn)無(wú)法被 GC 回收狈茉,這個(gè)時(shí)候就可能會(huì)產(chǎn)生內(nèi)存泄露
ThreadLocalMap 實(shí)現(xiàn) 中已經(jīng)考慮了這種情況弦撩,在調(diào)用 set() 、 get() 论皆、 remove() 方法的時(shí)候益楼,會(huì)清理掉 key 為 null 的記錄。使用完 ThreadLocal 方法后最好手動(dòng)調(diào)用 remove() 方法
測(cè)試代碼
這里引用的是博客[3]中的代碼
public class ThreadLocalTest {
// 測(cè)試代碼
public static void main(String[] args){
// 新開2個(gè)線程用于設(shè)置 & 獲取 ThreadLoacl的值
MyRunnable runnable = new MyRunnable();
new Thread(runnable, "線程1").start();
new Thread(runnable, "線程2").start();
}
// 線程類
public static class MyRunnable implements Runnable {
// 創(chuàng)建ThreadLocal & 初始化
private ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return "初始化值";
}
};
@Override
public void run() {
// 運(yùn)行線程時(shí)点晴,分別設(shè)置 & 獲取 ThreadLoacl的值
String name = Thread.currentThread().getName();
threadLocal.set(name + "的threadLocal"); // 設(shè)置值 = 線程名
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + ":" + threadLocal.get());
}
}
}
參考
[1]. 《java核心技術(shù) 卷1》
[2]. 《JavaGuide面試突擊版》
[3]. Java多線程:神秘的線程變量 ThreadLocal 你了解嗎感凤?
[4]. 強(qiáng)軟弱虛引用,只有體會(huì)過了粒督,才能記住