ThreadLocal 介紹
Java官方文檔中的描述:ThreadLocal 類用來提供線程內部的局部變量。這種變量在多線程環(huán)境下訪問(通過get和set方法訪問)時能保證各個線程的變量相對獨立于其它線程內的變量。ThreadLocal實例通常來說都是 private static 類型的桨踪,用于關聯(lián)線程和線程上下文讥巡。
我們知道 ThreadLocal 的作用是:提供線程內的局部變量掀亩,不同線程之間不會相互干擾,這種變量在線程的生命周期內起作用欢顷,減少同一個線程內多個函數(shù)或組件之間一些公共變量傳遞的復雜度槽棍。
總結:
1.線程并發(fā):在多線程并發(fā)的場景下
2.傳遞數(shù)據(jù):我們可以通過ThreadLocal在同一線程,不同組件中傳遞公共變量
3.線程隔離:每個線程的變量都是獨立的抬驴,不會相互影響
基本使用
常用方法
方法聲明 | 描述 |
---|---|
ThreadLocal() | 創(chuàng)建ThreadLocal對象 |
public void set(T value) | 設置當前線程綁定的局部變量 |
public T get() | 獲取當前線程綁定的局部變量 |
public void remove() | 移除當前線程綁定的局部變量 |
使用案例
我們先看下多線程并發(fā)時共享變量的錯誤案例:
package com.wangcp.test;
/**
* 需求:線程隔離
* 在多線程并發(fā)場景下炼七,每個線程中的變量都是相互獨立的
* 現(xiàn)場A:設置(變量1) 獲取(變量1)
* 線程B:設置(變量2) 獲炔汲帧(變量2)
* */
public class Demo01 {
// 設置變量
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
Demo01 demo = new Demo01();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(()->{
demo.setContent(Thread.currentThread().getName() + "的數(shù)據(jù)");
System.out.println("----------------------------");
System.out.println(Thread.currentThread().getName() + "-->" + demo.getContent());
});
thread.setName("線程"+ i);
thread.start();
}
}
}
運行結果如下:
我們知道Java的調度是一個搶占式調度豌拙,通過運行結果我們看出,多線程之間數(shù)據(jù)隔離的問題题暖,線程A取出了線程B的數(shù)據(jù)按傅。
接著我們加入ThreadLocal后再進行測試:
package com.wangcp.test;
/**
* 需求:線程隔離
* 在多線程并發(fā)場景下,每個線程中的變量都是相互獨立的
* 現(xiàn)場A:設置(變量1) 獲入事薄(變量1)
* 線程B:設置(變量2) 獲任ㄉ堋(變量2)
*
* ThreadLocal:
* 1.set() : 將變量綁定到當前線程中
* 2.get() : 獲取當前線程綁定的變量
* */
public class Demo01 {
ThreadLocal<String> t1 = new ThreadLocal<>();
// 設置變量
private String content;
public String getContent() {
// return content;
return t1.get();
}
public void setContent(String content) {
// this.content = content;
t1.set(content);
}
public static void main(String[] args) {
Demo01 demo = new Demo01();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(()->{
/**
* 每個線程:存一個變量,過一會后取出這個變量
*/
demo.setContent(Thread.currentThread().getName() + "的數(shù)據(jù)");
System.out.println("----------------------------");
System.out.println(Thread.currentThread().getName() + "-->" + demo.getContent());
});
thread.setName("線程"+ i);
thread.start();
}
}
}
運行結果如下:
在我們加入ThreadLocal后枝誊,實現(xiàn)了線程間數(shù)據(jù)的隔離况芒,線程取到的數(shù)據(jù)都是自身所設置的數(shù)據(jù)。
ThreadLocal類與synchronized關鍵字
synchronized 同步方式
可能有的朋友會覺得在上述的例子中我們完全可以通過加鎖來實現(xiàn)這個功能叶撒,我們來看下用synchronized代碼塊實現(xiàn)的效果:
package com.wangcp.test;
/**
* 需求:線程隔離
* 在多線程并發(fā)場景下绝骚,每個線程中的變量都是相互獨立的
* 現(xiàn)場A:設置(變量1) 獲取(變量1)
* 線程B:設置(變量2) 獲热(變量2)
* */
public class Demo02 {
// 設置變量
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
Demo02 demo = new Demo02();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(()->{
/**
* 每個線程:存一個變量皮壁,過一會后取出這個變量
*/
synchronized (Demo02.class){
demo.setContent(Thread.currentThread().getName() + "的數(shù)據(jù)");
System.out.println("----------------------------");
System.out.println(Thread.currentThread().getName() + "-->" + demo.getContent());
}
});
thread.setName("線程"+ i);
thread.start();
}
}
}
運行結果如下:
從運行結果看出,加鎖確實可以解決這個問題哪审,但是在這里我們強調的是線程數(shù)據(jù)隔離問題蛾魄,并不是多線程共享數(shù)據(jù)的問題,在這個案例中使用synchronized關鍵字是不合適的。
ThreadLocal與synchronized的區(qū)別
雖然 ThreadLocal 模式與 synchronized 關鍵字都用于處理多線程并發(fā)訪問變量的問題滴须,不過兩者處理問題的角度和思路不同舌狗。
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步機制采用“以時間換空間”的方式,只提供了一份變量扔水,讓不同的線程排隊訪問 | ThreadLocal采用“以空間換時間”的方式痛侍,為每一個線程都提供了一份變量的副本,從而實現(xiàn)同時訪問而不想干擾 |
側重點 | 多個線程之間訪問資源的同步 | 多線程中讓每個線程之間的數(shù)據(jù)相互隔離 |
總結:在上面的案例中魔市,雖然使用ThreadLocal和synchronized都能解決問題主届,但是使用ThreadLocal更為合適,因為這樣可以使程序擁有更高的并發(fā)性待德。
ThreadLocal 的內部結構
通過以上的學習君丁,我們對ThreadLocal的作用有了一定的認識。現(xiàn)在我們一起來看一下ThreadLocal的內部結構将宪,探究它能夠實現(xiàn)線程數(shù)據(jù)隔離的原理绘闷。
常見的誤解
如果我們不去看源代碼的話,可能會猜測ThreadLocal是這樣子設計的:每個ThreadLocal都創(chuàng)建一個Map较坛,然后用線程作為Map的key印蔗,要存儲的局部變量作為Map的value,這樣就能達到各個線程的局部變量隔離的效果丑勤。這是最簡單的設計方法华嘹,JDK最早期的ThreadLocal 確實是這樣設計的,但現(xiàn)在早已不是了确封。
現(xiàn)在的設計
但是除呵,JDK后面優(yōu)化了設計方案,在JDK8中 ThreadLocal的設計是:每個Thread維護一個ThreadLocalMap爪喘,這個Map的key是ThreadLocal實例本身颜曾,value才是真正要存儲的值Object。
具體的過程是這樣的:
每個Thread線程內部都有一個Map (ThreadLocalMap)
Map里面存儲ThreadLocal對象(key)和線程的變量副本(value)
Thread內部的Map是由ThreadLocal維護的秉剑,由ThreadLocal負責向map獲取和設置線程的變量值泛豪。
對于不同的線程,每次獲取副本值時侦鹏,別的線程并不能獲取到當前線程的副本值诡曙,形成了副本的隔離,互不干擾略水。
這樣設計的好處
這個設計與我們一開始說的設計剛好相反价卤,這樣設計有如下兩個優(yōu)勢:
這樣設計之后每個Map存儲的Entry數(shù)量就會變少。因為之前的存儲數(shù)量由Thread的數(shù)量決定渊涝,現(xiàn)在是由ThreadLocal的數(shù)量決定慎璧。在實際運用當中床嫌,往往ThreadLocal的數(shù)量要少于Thread的數(shù)量。
當Thread銷毀之后胸私,對應的ThreadLocalMap也會隨之銷毀厌处,能減少內存的使用。
ThreadLocal 的核心方法源碼
基于ThreadLocal的內部結構岁疼,我們繼續(xù)分析它的核心方法源碼阔涉,更深入的了解其操作原理。
除了構造方法之外捷绒, ThreadLocal對外暴露的方法有以下4個:
方法聲明 | 描述 |
---|---|
protected T initialValue() | 返回當前線程局部變量的初始值 |
public void set( T value) | 設置當前線程綁定的局部變量 |
public T get() | 獲取當前線程綁定的局部變量 |
public void remove() | 移除當前線程綁定的局部變量 |
以下是這4個方法的詳細源碼分析(為了保證思路清晰, ThreadLocalMap部分暫時不展開,下一個知識點詳解)
set方法
源碼和對應的中文注釋
/**
* 設置當前線程對應的ThreadLocal的值
* @param value 將要保存在當前線程對應的ThreadLocal的值
*/
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
// 1)當前線程Thread 不存在ThreadLocalMap對象
// 2)則調用createMap進行ThreadLocalMap對象的初始化
// 3)并將 t(當前線程)和value(t對應的值)作為第一個entry存放至ThreadLocalMap中
createMap(t, value);
}
/**
* 獲取當前線程Thread對應維護的ThreadLocalMap
*
* @param t the current thread 當前線程
* @return the map 對應維護的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
*創(chuàng)建當前線程Thread對應維護的ThreadLocalMap
*
* @param t 當前線程
* @param firstValue 存放到map中第一個entry的值
*/
void createMap(Thread t, T firstValue) {
//這里的this是調用此方法的threadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
代碼執(zhí)行流程
A. 首先獲取當前線程瑰排,并根據(jù)當前線程獲取一個Map
B. 如果獲取的Map不為空,則將參數(shù)設置到Map中(當前ThreadLocal的引用作為key)
C. 如果Map為空暖侨,則給該線程創(chuàng)建 Map凶伙,并設置初始值
get方法
源碼和對應的中文注釋
/**
* 返回當前線程中保存ThreadLocal的值
* 如果當前線程沒有此ThreadLocal變量,
* 則它會通過調用{@link #initialValue} 方法進行初始化值
*
* @return 返回當前線程對應此ThreadLocal的值
*/
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;
}
}
/*
初始化 : 有兩種情況有執(zhí)行當前代碼
第一種情況: map不存在,表示此線程沒有維護的ThreadLocalMap對象
第二種情況: map存在, 但是沒有與當前ThreadLocal關聯(lián)的entry
*/
return setInitialValue();
}
/**
* 初始化
*
* @return the initial value 初始化后的值
*/
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
// 1)當前線程Thread 不存在ThreadLocalMap對象
// 2)則調用createMap進行ThreadLocalMap對象的初始化
// 3)并將 t(當前線程)和value(t對應的值)作為第一個entry存放至ThreadLocalMap中
createMap(t, value);
// 返回設置的值value
return value;
}
代碼執(zhí)行流程
A. 首先獲取當前線程, 根據(jù)當前線程獲取一個Map
B. 如果獲取的Map不為空显押,則在Map中以ThreadLocal的引用作為key來在Map中獲取對應的Entry e扳肛,否則轉到D
C. 如果e不為null,則返回e.value乘碑,否則轉到D
D. Map為空或者e為空挖息,則通過initialValue函數(shù)獲取初始值value,然后用ThreadLocal的引用和value作為firstKey和firstValue創(chuàng)建一個新的Map
總結: 先獲取當前線程的 ThreadLocalMap 變量兽肤,如果存在則返回值套腹,不存在則創(chuàng)建并返回初始值。
remove方法
源碼和對應的中文注釋
/**
* 刪除當前線程中保存的ThreadLocal對應的實體entry
*/
public void remove() {
// 獲取當前線程對象中維護的ThreadLocalMap對象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null)
// 存在則調用map.remove
// 以當前ThreadLocal為key刪除對應的實體entry
m.remove(this);
}
代碼執(zhí)行流程
A. 首先獲取當前線程资铡,并根據(jù)當前線程獲取一個Map
B. 如果獲取的Map不為空电禀,則移除當前ThreadLocal對象對應的entry
initialValue方法
/**
* 返回當前線程對應的ThreadLocal的初始值
* 此方法的第一次調用發(fā)生在,當線程通過get方法訪問此線程的ThreadLocal值時
* 除非線程先調用了set方法笤休,在這種情況下尖飞,initialValue 才不會被這個線程調用。
* 通常情況下店雅,每個線程最多調用一次這個方法政基。
*
* <p>這個方法僅僅簡單的返回null {@code null};
* 如果程序員想ThreadLocal線程局部變量有一個除null以外的初始值,
* 必須通過子類繼承{@code ThreadLocal} 的方式去重寫此方法
* 通常, 可以通過匿名內部類的方式實現(xiàn)
*
* @return 當前ThreadLocal的初始值
*/
protected T initialValue() {
return null;
}
此方法的作用是 返回該線程局部變量的初始值闹啦。
(1) 這個方法是一個延遲調用方法沮明,從上面的代碼我們得知,在set方法還未調用而先調用了get方法時才執(zhí)行窍奋,并且僅執(zhí)行1次荐健。
(2)這個方法缺省實現(xiàn)直接返回一個null酱畅。
(3)如果想要一個除null之外的初始值,可以重寫此方法摧扇。(備注: 該方法是一個protected的方法圣贸,顯然是為了讓子類覆蓋而設計的)
ThreadLocalMap 源碼分析
在分析ThreadLocal方法的時候,我們了解到ThreadLocal的操作實際上是圍繞ThreadLocalMap展開的扛稽。ThreadLocalMap的源碼相對比較復雜, 我們從以下三個方面進行討論吁峻。
基本結構
ThreadLocalMap是ThreadLocal的內部類,沒有實現(xiàn)Map接口在张,用獨立的方式實現(xiàn)了Map的功能用含,其內部的Entry也是獨立實現(xiàn)。
成員變量
/**
* 初始容量 —— 必須是2的整次冪
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 存放數(shù)據(jù)的table帮匾,Entry類的定義在下面分析
* 同樣啄骇,數(shù)組長度必須是2的整次冪。
*/
private Entry[] table;
/**
* 數(shù)組里面entrys的個數(shù)瘟斜,可以用于判斷table當前使用量是否超過閾值缸夹。
*/
private int size = 0;
/**
* 進行擴容的閾值,表使用量大于它的時候進行擴容螺句。
*/
private int threshold; // Default to 0
跟HashMap類似虽惭,INITIAL_CAPACITY代表這個Map的初始容量;table 是一個Entry 類型的數(shù)組蛇尚,用于存儲數(shù)據(jù)芽唇;size 代表表中的存儲數(shù)目; threshold 代表需要擴容時對應 size 的閾值
存儲結構 - Entry
/*
* Entry繼承WeakReference取劫,并且用ThreadLocal作為key.
* 如果key為null(entry.get() == null)匆笤,意味著key不再被引用,
* 因此這時候entry也可以從table中清除谱邪。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
在ThreadLocalMap中炮捧,也是用Entry來保存K-V結構數(shù)據(jù)的。不過Entry中的key只能是ThreadLocal對象惦银,這點在構造方法中已經限定死了寓盗。
另外,Entry繼承WeakReference璧函,也就是key(ThreadLocal)是弱引用傀蚌,其目的是將ThreadLocal對象的生命周期和線程生命周期解綁。
弱引用和內存泄漏
有些程序員在使用ThreadLocal的過程中會發(fā)現(xiàn)有內存泄漏的情況發(fā)生蘸吓,就猜測這個內存泄漏跟Entry中使用了弱引用的key有關系善炫。這個理解其實是不對的。
我們先來回顧這個問題中涉及的幾個名詞概念库继,再來分析問題箩艺。
內存泄漏相關概念
- Memory overflow:內存溢出窜醉,沒有足夠的內存提供申請者使用。
- Memory leak: 內存泄漏是指程序中已動態(tài)分配的堆內存由于某種原因程序未釋放或無法釋放艺谆,造成系統(tǒng)內存的浪費榨惰,導致程序運行速度減慢甚至系統(tǒng)崩潰等嚴重后果。內存泄漏的堆積終將導致內存溢出静汤。
弱引用相關概念
Java中的引用有4種類型: 強琅催、軟、弱虫给、虛藤抡。當前這個問題主要涉及到強引用和弱引用:
強引用(“Strong” Reference),就是我們最常見的普通對象引用抹估,只要還有強引用指向一個對象缠黍,就能表明對象還“活著”,垃圾回收器就不會回收這種對象药蜻。
弱引用(WeakReference)瓷式,垃圾回收器一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當前內存空間足夠與否语泽,都會回收它的內存蒿往。
如果key使用強引用
假設ThreadLocalMap中的key使用了強引用,那么會出現(xiàn)內存泄漏嗎湿弦?
此時ThreadLocal的內存圖(實線表示強引用)如下:
假設在業(yè)務代碼中使用完ThreadLocal ,threadLocal Ref被回收了腾夯。
但是因為threadLocalMap的Entry強引用了threadLocal颊埃,造成threadLocal無法被回收。
在沒有手動刪除這個Entry以及CurrentThread依然運行的前提下蝶俱,始終有強引用鏈 threadRef->currentThread->threadLocalMap->entry班利,Entry就不會被回收(Entry中包括了ThreadLocal實例和value),導致Entry內存泄漏榨呆。
也就是說罗标,ThreadLocalMap中的key使用了強引用, 是無法完全避免內存泄漏的积蜻。
如果key使用弱引用
那么ThreadLocalMap中的key使用了弱引用闯割,會出現(xiàn)內存泄漏嗎?
此時ThreadLocal的內存圖(實線表示強引用竿拆,虛線表示弱引用)如下:
同樣假設在業(yè)務代碼中使用完ThreadLocal 宙拉,threadLocal Ref被回收了。
由于ThreadLocalMap只持有ThreadLocal的弱引用丙笋,沒有任何強引用指向threadlocal實例, 所以threadlocal就可以順利被gc回收谢澈,此時Entry中的key=null煌贴。
但是在沒有手動刪除這個Entry以及CurrentThread依然運行的前提下,也存在有強引用鏈 threadRef->currentThread->threadLocalMap->entry -> value 锥忿,value不會被回收牛郑, 而這塊value永遠不會被訪問到了,導致value內存泄漏敬鬓。
也就是說淹朋,ThreadLocalMap中的key使用了弱引用, 也有可能內存泄漏列林。
出現(xiàn)內存泄漏的真實原因
比較以上兩種情況瑞你,我們就會發(fā)現(xiàn),內存泄漏的發(fā)生跟ThreadLocalMap中的key是否使用弱引用是沒有關系的希痴。那么內存泄漏的的真正原因是什么呢者甲?
細心的同學會發(fā)現(xiàn),在以上兩種內存泄漏的情況中砌创,都有兩個前提:
1.沒有手動刪除這個Entry
2.CurrentThread依然運行
第一點很好理解虏缸,只要在使用完ThreadLocal,調用其remove方法刪除對應的Entry嫩实,就能避免內存泄漏刽辙。
第二點稍微復雜一點, 由于ThreadLocalMap是Thread的一個屬性甲献,被當前線程所引用宰缤,所以它的生命周期跟Thread一樣長。那么在使用完ThreadLocal之后晃洒,如果當前Thread也隨之執(zhí)行結束慨灭,ThreadLocalMap自然也會被gc回收,從根源上避免了內存泄漏球及。
綜上氧骤,ThreadLocal內存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動刪除對應key就會導致內存泄漏吃引。
為什么使用弱引用
根據(jù)剛才的分析, 我們知道了: 無論ThreadLocalMap中的key使用哪種類型引用都無法完全避免內存泄漏筹陵,跟使用弱引用沒有關系。
要避免內存泄漏有兩種方式:
使用完ThreadLocal镊尺,調用其remove方法刪除對應的Entry
使用完ThreadLocal朦佩,當前Thread也隨之運行結束
相對第一種方式,第二種方式顯然更不好控制庐氮,特別是使用線程池的時候吕粗,線程結束是不會銷毀的。
也就是說旭愧,只要記得在使用完ThreadLocal及時的調用remove,無論key是強引用還是弱引用都不會有問題。那么為什么key要用弱引用呢庶灿?
事實上先紫,在ThreadLocalMap中的set/getEntry方法中,會對key為null(也即是ThreadLocal為null)進行判斷,如果為null的話,那么是會對value置為null的。
這就意味著使用完ThreadLocal型奥,CurrentThread依然運行的前提下,就算忘記調用remove方法碉京,弱引用比強引用可以多一層保障:弱引用的ThreadLocal會被回收厢汹,對應的value在下一次ThreadLocalMap調用set,get,remove中的任一方法的時候會被清除,從而避免內存泄漏谐宙。
本篇博客是觀看B站黑馬關于ThreadLocal 的視頻后進行整理的烫葬,相當于是對應視頻的筆記。如有問題歡迎大家多多建議和指正凡蜻。