Java中的ThreadLocal與ThreadLocalMap詳解
ThreadLocal與ThreadLocalMap(jdk 1.8)
使用場景
- 每個線程需要一個獨享的對象(通常是工具類)
- 每個線程內需要保存全局變量,可以在不同的地方直接獲取,避免參數(shù)傳遞的麻煩
作用
- 讓某個需要用到的對象在線程間隔離(每個線程都有自己獨享的對象)
- 任何方法中都可以輕松獲取其對象
好處
- 可以達到線程安全
- 不需要加鎖,提高效率
- 高效利用內存,相比于每個任務都新建一個對象,用ThreadLocal可以節(jié)省內存和開銷
- 免去傳遞參數(shù)的繁瑣,降低了程序耦合度
主要方法
1)initialValue()
該方法會返回當前線程對應的初始值,采用了懶加載機制,當?shù)谝淮蝕et的時候才會觸發(fā),當線程第一次使用get方法的時候才會觸發(fā)。除非線程先前調用了set方法,在這種情況下,不會再調用InitValue方法
2)set(T value)
未當前線程設置一個新的值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}T get()
public T get() {
Thread t = Thread.currentThread();//獲取當前線程
ThreadLocalMap map = getMap(t);//從當前線程中獲取ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//獲取Entry
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;//返回對象
}
}
return setInitialValue();//如果第一次調用get,ThreadLocalMap未空或者在ThreadLocalMap中還未存儲對象,則進行初始化并返回存儲對象
}remove()
//移除線程所存儲對象
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}原理
在Thread類中又這樣一個ThreadLocalMap 類型成員變量threadLocals
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap 是ThreadLocal的內部類,其結構如HashMap很相似,在其內部還有個Entry,保存ThreadLocal和其保存的對象。其默認容量也為16,負載因子未2/3,并且不存在next指針,哈希沖突后采用的延后策略。具體請看最后問題欄
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold; //閾值
private void setThreshold(int len) {
threshold = len * 2 / 3; //負載因子是2/3,
}
//......省略............
}
總的來說,在Thead中維護了一個Map,在Map中存儲了ThreadLocal和其綁定的對象
每次獲取對象都會從當前線程中獲取map并將ThreadLocal傳入從而獲得對象
內存泄露
ThreadLocal被用作TheadLocalMap的弱引用key,這種設計也是ThreadLocal被討論內存泄露的熱點問題,因此有必要了解一下什么是弱引用。
弱引用
弱引用是用來描述非必須的對象的,但它的強度比軟引用更弱,被弱引用關聯(lián)的對象只能生存到下一次GC發(fā)生之前,也就是說下一次GC就會被回收。JDK1.2之后,提供了WeakReference來實現(xiàn)弱引用。
? 由于ThreadLocalMap是以弱引用的方式引用著ThreadLocal,換句話說,就是ThreadLocal是被ThreadLocalMap以弱引用的方式關聯(lián)著,因此如果ThreadLocal沒有被ThreadLocalMap以外的對象引用,則在下一次GC的時候,ThreadLocal實例就會被回收,那么此時ThreadLocalMap里的一組KV的K就是null了,因此在沒有額外操作的情況下,此處的V便不會被外部訪問到,而且只要Thread實例一直存在,Thread實例就強引用著ThreadLocalMap,因此ThreadLocalMap就不會被回收,那么這里K為null的V就一直占用著內存。
綜上,發(fā)生內存泄露的條件是
- ThreadLocal實例沒有被外部強引用,比如我們假設在提交到線程池的task中實例化的ThreadLocal對象,當task結束時,ThreadLocal的強引用也就結束了
- ThreadLocal實例被回收,但是在ThreadLocalMap中的V沒有被任何清理機制有效清理
- 當前Thread實例一直存在,則會一直強引用著ThreadLocalMap,也就是說ThreadLocalMap也不會被GC
示例
class Test{
byte data[]=new byte[1024*1024*10];
@Override
protected void finalize() throws Throwable {
System.out.println("destroy");
}
}
public class ThreadLocalDemo {
public ThreadLocal<Test> t = new ThreadLocal<>();
public static void main(String[] args) {
ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
Test test = new Test();
threadLocalDemo.t.set(test);
test = null;
//threadLocalDemo.t.remove();
threadLocalDemo = null;
System.out.println("start gc");
System.gc();
try {
Thread.sleep(1000L);
}catch (Exception e) {
e.printStackTrace();
}
System.out.println("end");
}
}輸出
/*
start gc
end
*/
//當threadLocalDemo.t.remove();不被注釋
/*
輸出:
start gc
destroy
end
*/
當不在持有ThreadLocalDemo對象,因為thread中ThreadLoaclMap中保存有ThreadLocal的引用 ,如果ThreadLocal不是弱引用的話,ThreadLocal是不可能被gc的。而如果ThreadLocal與ThreadLocalMap之間是弱引用,如果除Thread外沒有任何對象可以獲得ThreadLocal,則ThreadLocal是可以為回收的
? 當然,其仍然仍然存在一定的內存泄露,即value與TreadLcoalMap之間存在引用,當ThreadLocal被gc時value是無法被gc的,但是在ThreadLocalMap內部也存在一些機制,當map擴容或者發(fā)生hash沖突的時候會判斷key鍵是否為null(即判斷ThreadLocal對象是否被回收),如果是null,則會將value值同樣設為Null.從而幫助value gc
ThreadLocal為什么經常設置為static
public class ThreadLocalDemo2 {
public ThreadLocal<Test> t = new ThreadLocal<>();
//public static ThreadLocal<Test> t = new ThreadLocal<>();
public static void main(String[] args) {
ThreadLocalDemo2 threadLocalDemo = new ThreadLocalDemo2();
Test test = new Test();
test.name = "xxxx";
threadLocalDemo.t.set(test);
ThreadLocalDemo2 threadLocalDemo2 = new ThreadLocalDemo2();
Test test2 = new Test();
test2.name = "yyyyy";
threadLocalDemo2.t.set(test2);
System.out.println(threadLocalDemo.t.get().name);
System.out.println(threadLocalDemo2.t.get().name);
}
}/*
輸出:
xxxx
yyyyy
static 修飾 ThreadLocal
輸出:
yyyyy
yyyyy
*/
static修飾ThreadLocal后,單個線程無論創(chuàng)建多個對象,其ThreadLocal示例僅僅只有一個。
如果變量ThreadLocal是非static的就會造成每次生成實例都要生成不同的ThreadLocal對象,雖然這樣程序不會有什么異常,但是會浪費內存資源,甚至會造成內存泄漏.。
建議
- 通過前面幾節(jié)的分析,我們基本弄清楚了ThreadLocal相關設計和內存模型,對于是否會發(fā)生內存泄露做了分析,下面總結下幾點建議:
- 當需要存儲線程私有變量的時候,可以考慮使用ThreadLocal來實現(xiàn)
- 當需要實現(xiàn)線程安全的變量時,可以考慮使用ThreadLocal來實現(xiàn)
- 當需要減少線程資源競爭的時候,可以考慮使用ThreadLocal來實現(xiàn)
- 注意Thread實例和ThreadLocal實例的生存周期,因為他們直接關聯(lián)著存儲數(shù)據(jù)的生命周期
- 如果頻繁的在線程中new ThreadLocal對象,在使用結束時,最好調用ThreadLocal.remove來釋放其value的引用,避免在ThreadLocal被回收時value無法被訪問卻又占用著內存
問題:
為什么ThreadLocalMap不用HashMap而是自己寫了個Map
- 自定義Map限定了鍵值未ThreadLocal類型
- 其Entry對象繼承了弱引用類,用來存儲鍵值,從而不影響對象被回收,而HashMap中Key是強引用
- ThreadLocalMap在寫數(shù)據(jù)和查數(shù)據(jù)的過程中有一個清理過期數(shù)據(jù)的功能,能夠將發(fā)現(xiàn)的過期數(shù)據(jù)清理到,從某種意義上也是解決了內存泄漏問題。當然不是完全解決
ThreadLocalMap達到擴容的閾值時會真正的擴容嗎?
不會,達到閾值之后,進行一個散列表的掃描清楚過期的數(shù)據(jù),如果清理完之后,數(shù)據(jù)量仍然達到其閾值的75%,才進行擴容
擴容源碼:
private void rehash() {
expungeStaleEntries();//清理
if (size >= threshold - threshold / 4)//數(shù)據(jù)量仍然達到其閾值的75%,才進行擴容
resize();
}
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];//新建一個數(shù)組
int count = 0;
for (int j = 0; j < oldLen; ++j) {//遍歷
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; //將value設為null從而幫助GC
} else {
//重新進行hash
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);//采用的時自定義hash算法
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);//計算新的閾值
size = count;
table = newTab;
}ThreadLocalMap獲取Entry的流程
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);//hash運算計算出位置
Entry e = table[i];
if (e != null && e.get() == key)//未發(fā)生過Hash沖突
return e;
else//發(fā)生過沖突
return getEntryAfterMiss(key, i, e);//進行下一個位置的判斷
}
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)//如果為空,則說明此位置被GC了,為過期數(shù)據(jù)
expungeStaleEntry(i);//為了防止內存泄漏,觸發(fā)一個“探測式”過期數(shù)據(jù)回收邏輯
else
i = nextIndex(i, len);//計算下一個位置
e = tab[i];
}
return null;
}
//“探測式”過期數(shù)據(jù)回收邏輯
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null;//將value設為空,幫助GC
tab[staleSlot] = null;
size--;
Entry e;
int i;
for (i = nextIndex(staleSlot, len);//根據(jù)hash和尋址算法遍歷所有與當前hash相同的槽點
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {//幫助GC
e.value = null;
tab[i] = null;
size--;
} else {
//如果key不為空,則重新進行hash,將其移動到一個更靠近其hash位置的槽點(提高下次get的效率)
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}ThreadLocalMap中set的具體流程
private void set(ThreadLocal<?> key, Object value) {
//尋址
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//遍歷可能的slot
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果key相同,則替換
if (k == key) {
e.value = value;
return;
}
//如果k為空,則進行取代算法
if (k == null) {
//大體就是遍歷可能的槽點,直到碰到key值相同的,則將其移動到距離真實hash位置最近的點,如果沒有,則再最有好的位置new一個新的Entry
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;//判斷是否達到擴容條件
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}到此這篇關于Java中的ThreadLocal與ThreadLocalMap詳解的文章就介紹到這了,更多相關ThreadLocal與ThreadLocalMap內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
解決BeanUtils.copyProperties不支持復制集合的問題
這篇文章主要介紹了解決BeanUtils.copyProperties不支持復制集合的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06
Spring AOP定義AfterReturning增加實例分析
這篇文章主要介紹了Spring AOP定義AfterReturning增加,結合實例形式分析了Spring面相切面AOP定義AfterReturning增加相關操作技巧與使用注意事項,需要的朋友可以參考下2020-01-01
spring boot @ResponseBody轉換JSON 時 Date 類型處理方法【兩種方法】
這篇文章主要介紹了spring boot @ResponseBody轉換JSON 時 Date 類型處理方法,主要給大家介紹Jackson和FastJson兩種方式,每一種方法給大家介紹的都非常詳細,需要的朋友可以參考下2018-08-08
mybatis-flex與springBoot整合的實現(xiàn)示例
Mybatis-flex提供了簡單易用的API,開發(fā)者只需要簡單的配置即可使用,本文主要介紹了mybatis-flex與springBoot整合,具有一定的參考價值,感興趣的可以了解一下2024-01-01
如何使用 IntelliJ IDEA 編寫 Spark 應用程序(Sc
本教程展示了如何在IntelliJIDEA中使用Maven編寫和運行一個簡單的Spark應用程序(例如WordCount程序),本文通過實例代碼給大家介紹的非常詳細,感興趣的朋友跟隨小編一起看看吧2024-11-11

