基于ThreadLocal 的用法及內(nèi)存泄露(內(nèi)存溢出)
ThreadLocal 看名字 就可以看出一點(diǎn)頭緒來,線程本地。
來看一下java對他的描述:
該類提供線程本地變量。這些變量與它們的正常對應(yīng)變量的不同之處在于,每個線程(通過ThreadLocal的 get 或 set方法)訪問自己的、獨(dú)立初始化的變量副本。 ThreadLocal實(shí)例通常是類中的私有靜態(tài)字段。
上面這段話呢,一個重點(diǎn)就是 每個線程都有自己的專屬變量,這個專屬變量呢,是不會被其他線程影響的。
使用
public class ThreadLocalTwo {
//靜態(tài)的 延長生命周期。final 不可改變
private static final ThreadLocal<Integer> threalLocal = ThreadLocal.withInitial(() -> {
return 0;
});
public static void main(String[] args) {
new Thread(() -> {
while (true) {
//取出來
int inner = threalLocal.get();
//使用
System.out.println(Thread.currentThread().getName() + " " + inner);
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
//更新值存入
threalLocal.set(++inner);
}
}, "three").start();
new Thread(() -> {
while (true) {
//取出來
int inner = threalLocal.get();
//使用
System.out.println(Thread.currentThread().getName() + " " + inner);
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
//更新值存入
threalLocal.set(++inner);
}
}, "four").start();
}
}
使用這個我只是隨便寫一個demo,具體的邏輯有很多種,只要你想,就會有很多種寫法。具體看業(yè)務(wù)需求。
個人理解
ThreadLocal 類似于一個工具,通過這個工具,來為當(dāng)前線程設(shè)定修改移除本地副本。,如果 你查看Thread的源碼會發(fā)現(xiàn)下面這段代碼
/* ThreadLocal values pertaining to this thread. This map is maintained
by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
這是靜態(tài)內(nèi)部類構(gòu)造的一個字段,那么我們看一下 ThreadLocal.ThreadLocalMap的源碼.
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
上面代碼我們可以發(fā)現(xiàn) ThreadLocal.ThreadLocalMap這個內(nèi)部靜態(tài)類,里面還包含這一個內(nèi)部靜態(tài)類Entry。
這個Entry 繼承了WeakReference,并且將ThreadLocal作為弱引用類型。這表明 ThreadLocal如果沒有其他的強(qiáng)引用時候,說不定 有可能不知道啥時候就被回收了。
那么至于 value呢? 我可以肯定的告訴你 value不會被回收,即便 傳進(jìn)來的v是個匿名類。
value持有著線程的本地副本的引用
Entry[] table 這個持有 entry的引用
現(xiàn)在 ,只需要知道
1 弱引用對象,會持有引用對象的引用,弱引用對象并不能決定 引用對象是否回收。
2 弱引用的子類的 如果有自己的字段的話, 那么那個字段是強(qiáng)引用,不會被回收
3 弱引用對象,如果是new出來的,那么弱引用對象本身也是一個強(qiáng)引用。弱引用對象自己不會被回收。
構(gòu)造方法
一個默認(rèn)的無參構(gòu)造方法 ,沒啥好講的,,
public ThreadLocal() {
}
使用
private static final ThreadLocal<String> construct = new ThreadLocal<>(){
//如果 不重寫這個方法的話,默認(rèn)返回null
@Override
protected String initialValue() {
return "默認(rèn)值";
}
};
靜態(tài)方法
note Java8新增的方法
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
上面的這個靜態(tài)方法呢,生成一個ThreadLocal對象,參數(shù)是一個Supplier函數(shù)接口。
下面展示一個代碼
private static final ThreadLocal<String> local = ThreadLocal.withInitial(() -> "默認(rèn)值");
上面這段代碼使用了Lambda表達(dá)式, 比起上面 new 并且重寫方法的寫法,代碼會少很多,顯得很有逼格對不。
如果你對java8的Lambda不清楚的話,可以看這篇文章:java Lambda表達(dá)式的使用
公共方法
//返回當(dāng)前線程本地副本的值。如果本地副本為null,則返回初始化為調(diào)用{@link #initialValue}方法返回的值。
public T get()
//將當(dāng)前線程的本地副本 設(shè)為 value
public void set(T value)
//將當(dāng)前線程的本地副本移除,如果后面調(diào)用get()方法的話,會返回T initialValue()的值
public void remove()
內(nèi)存泄露
接下來講一下,ThreadLocal配合線程池時候 會出現(xiàn)內(nèi)存泄漏的原理。按照我的個人理解 ,是因?yàn)閮?nèi)存溢出造成的。內(nèi)存泄露指的是 原本應(yīng)該回收的對象,現(xiàn)在由于種種原因,無法被回收。
為什么上面會強(qiáng)調(diào) 配合線程池的時候,因?yàn)閱为?dú)線程的時候,當(dāng)線程任務(wù)運(yùn)行完以后,線程資源會被回收,自然 本地副本也被回收了。而線程池里面的線程不全被回收(有的不會被回收,也有的會被回收)。
現(xiàn)在來看一下上面的Entry這個最終存儲本地副本的靜態(tài)內(nèi)部類,
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
下面內(nèi)容需要你對 java 內(nèi)存管理關(guān)系了解,否則 你肯定會一臉蒙蔽。
如果 你不會 可以看我這篇文章java內(nèi)存管理關(guān)系及內(nèi)存泄露的原理
由于它是WeakReference的子類,所以 作為引用對象的 ThreadLocal,就有可能會被Entry清除引用。如果這時候 ThreadLocal沒有其他的引用,那么它肯定就會被GC回收了。
但是value 是強(qiáng)引用,而Entry 又被Entry[]持有,Entry[]又被ThreadLocalMap持有,ThreadLocalMap又被線程持有。只要線程不死或者 你不調(diào)用set,remove這兩個方法之中任何一個,那么value指向的這個對象就始終 不會被回收。因?yàn)?不符合GC回收的兩個條件的任何一個。
試想一下如果線程池里面的線程足夠的多,并且 你傳給線程的本地副本內(nèi)存占用又很大。毫無疑問 會內(nèi)存溢出。
解決方法
只要調(diào)用remove 這個方法會擦出 上一個value的引用,這樣線程就不會持有上一個value指向?qū)ο蟮囊谩>筒粫袃?nèi)存露出了。
有讀者會有疑問了,上面不是說兩個放過會使value對象可以回收么,怎么上面沒有set方法呢?
這個是因?yàn)椋瑂et方法確實(shí)可以是value指向的對象 這個引用斷開,但同時它又強(qiáng)引用了一個內(nèi)存空間給value。即使上一個對象被回收了,但是新對象也產(chǎn)生了。
至于 get方法,只有在ThreadLocalMap 被GC后,調(diào)用get方法 才會將value對應(yīng)的引用切斷。
首先,我們看get源碼
public T get() {
Thread t = Thread.currentThread();//當(dāng)前線程的引用
//得到當(dāng)前線程的ThreadLocalMap,如果沒有返回null
ThreadLocalMap map = getMap(t);
//存在時候走這個
if (map != null) {
//與鍵關(guān)聯(lián)的項(xiàng),如果沒有鍵則為null
//如果ThreadLocalMap的entry 清除了ThreadLocal 對象的引用,那么這個會清除對應(yīng)的value 引用
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//當(dāng)前線程 沒有設(shè)置ThreadLocalMap,那么返回initialValue()的值
return setInitialValue();
}
上面這段代碼,調(diào)用了getEntry,這個方法內(nèi)部調(diào)用了 另一個方法,實(shí)現(xiàn)了當(dāng)ThreadLocal被清除引用后,也清除對應(yīng)的value引用,
private Entry getEntry(ThreadLocal<?> key) {
//得到位置 table數(shù)組 的容量是16
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//key沒有被回收后
if (e != null && e.get() == key)
return e;
else
//這個key被回收 調(diào)用,將對應(yīng)的value 釋放引用
return getEntryAfterMiss(key, i, e);
}
我們看見最后調(diào)用 getEntryAfterMiss(key, i, e),這個方法 也不是最終的擦除value引用的方法,我們接著往下看
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
//得到弱引用對象 持有的引用對象的引用
ThreadLocal<?> k = e.get();
//ThreadLocal沒有被回收
if (k == key)
return e;
if (k == null)
//entry 清除ThreadLocal的引用
//通過entry[]數(shù)組的元素entry 清除entry的value引用
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
這上面呢,我們要關(guān)注expungeStaleEntry(i),這個才是最終的擦除entry的value對象的引用。 看一下 expungeStaleEntry(i)的源碼
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;//得到table引用
int len = tab.length;//得到table的長度,不出意外 應(yīng)該是16
// expunge entry at staleSlot
//下面兩句代碼 是關(guān)鍵。
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;
}
上面這段代碼很長,我們不必細(xì)看個,關(guān)注下面這兩行代碼就行
tab[staleSlot].value = null;//清除引用 這樣 GC就可以回收了
tab[staleSlot] = null;//清除自身的引用
通過entry[staleSlot]得到存儲的entry ,通過entry清除entry的value引用。
這樣大家明白了吧,get也是可以起到和remove一樣的效果的。
我們再看一下remove的源碼
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
上面這段代碼沒什么說的,直接看ThreadLocalMap的remove方法
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
//得到位置,因?yàn)榇娴臅r候 也是按照這個規(guī)則來的,
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//這里有可能會發(fā)生 ThreadLocal 被entry清除引用,那么value就被線程引用了,如果不調(diào)用set,get方法的話,只能等待線程銷毀。
if (e.get() == key) {
//調(diào)用弱引用的方法 , 將引用對象的引用清除
e.clear();
//擦出ThreadLocal 對應(yīng)的value
expungeStaleEntry(i);
return;
}
}
}
上面調(diào)用了 expungeStaleEntry 擦除。
set
我們關(guān)注這個方法
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
//擦除
expungeStaleEntry(j);
}
}
這個呢 循環(huán)調(diào)用了expungeStaleEntry(j)方法 ,也是擦除了value的對象引用。
為什么要將ThreadLocal 定義成 static 變量
延長生命周期,之所以是static 是因?yàn)?,ThreadLocal 我們更應(yīng)該將他看成是 工具。
對ThreadLocal內(nèi)存泄漏引起的思考
概述
最近在對一個項(xiàng)目進(jìn)行重構(gòu),用到了ThreadLocal。
場景如下:
外圍系統(tǒng)會調(diào)用接口上傳數(shù)據(jù),在接口中要記錄數(shù)據(jù)的變化Id,在上傳數(shù)據(jù)完后需要集中在一個地方把這些Id以消息形式發(fā)送出去。
使用場景樣例代碼
public Result<Void> uploadOrder(TotalPayInfoVo totalPayInfoVo) {
try {
saveTotalPayInfoVo(totalPayInfoVo);
//發(fā)送消息
UnitWork.getCurrent().pushMessage();
} catch (Exception e) {
cashLogger.error("uploadOrder error,data: {}, error: {}", JSON.toJSONString(totalPayInfoVo), e);
throw new RuntimeException("保存失敗", e);
} finally {
UnitWork.clean();//
}
return ResultUtil.successResult();避免內(nèi)存泄漏
}
ThreadLocal使用源碼
/**
* 工作單元,在同一個線程中負(fù)責(zé)記錄一個事件或者一個方法或者一個事務(wù)過程中產(chǎn)生的變化,等操作結(jié)束后再處理這種變化。
*/
public class UnitWork {
private UnitWork() {
}
private static ThreadLocal<UnitWork> current = new ThreadLocal<UnitWork>() {
protected UnitWork initialValue() {
return new UnitWork();
}
};
/**
* 狀態(tài)變化的instance
*/
private Set<String> statusChangedInstances = new HashSet<>();
public void addStatusChangedInstance(String instance) {
statusChangedInstances.add(instance);
}
/**
* 推送消息
*/
public void pushMessage() {
for(String id : statusChangedInstances){
//異步發(fā)消息
}
}
public static UnitWork getCurrent() {
return current.get();
}
/**
* 刪除當(dāng)前線程的工作單元,建議放在finally中調(diào)用,避免內(nèi)存泄漏
*/
public static void clean() {
current.remove();
}
}
思考問題
為了避免內(nèi)存泄漏,每次用完做一下clean清理操作。發(fā)送消息的過程是異步的,意味著clean的時候可能和發(fā)送消息同時進(jìn)行。那么會不會把這些Id清理掉?那么可能造成消息發(fā)送少了。要回答這個問題,首先要搞懂ThreadLocal的引用關(guān)系,remove操作做了什么?
ThreadLocal解讀
ThreadLocal可以分別在各個線程保存變量獨(dú)立副本。每個線程都有ThreadLocalMap,顧名思義,類似Map容器,不過是用數(shù)組Entry[]來模擬的。那么既然類似Map,肯定會存在Key。其實(shí)Key是ThreadLocal類型,Key的值是ThreadLocal的HashCode,即通過threadLocalHashCode計(jì)算出來的值。
這個Map的Entry并不是ThreadLocal,而是一個帶有弱引用的Entry。既然是弱引用,每次GC的時候都會回收。
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
而Key對應(yīng)的value就是要保存在線程副本Object,這里指的就是UnitWork的實(shí)例。調(diào)用ThreadLocal的get方法時,首先找到當(dāng)前線程的ThreadLocalMap,然后根據(jù)這個ThreadLocal算出來的hashCode找到保存線程副本Object。
他們的關(guān)系對應(yīng)如下:

ThreadLocal在remove的時候,會調(diào)用Entry的clear,即弱引用的clear方法。把Key->ThreadLocal的引用去掉。接下來的expungeStaleEntry會把entry中value引用設(shè)置為null。
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
現(xiàn)在可以回答之前提前的問題。雖然ThreadLocal和當(dāng)前線程都會與Object脫離了引用的關(guān)系,但是最重要一點(diǎn)就是異步的線程仍然存在一條強(qiáng)引用路徑到Object,即到UnitWork實(shí)例的強(qiáng)引用。因此GC然后不會回收UnitWork的實(shí)例,發(fā)消息還是不會少發(fā)或者出現(xiàn)空指針情況。
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java springboot接口迅速上手,帶你半小時極速入門
這篇文章主要給大家介紹了關(guān)于SpringBoot實(shí)現(xiàn)API接口的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-09-09
Spring Cloud Gateway全局異常處理的方法詳解
這篇文章主要給大家介紹了關(guān)于Spring Cloud Gateway全局異常處理的相關(guān)資料,需要的朋友可以參考下2018-10-10
Java查詢時間段(startTime--endTime)間的數(shù)據(jù)方式
這篇文章主要介紹了Java查詢時間段(startTime--endTime)間的數(shù)據(jù)方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03
Java 實(shí)戰(zhàn)項(xiàng)目錘煉之在線美食網(wǎng)站系統(tǒng)的實(shí)現(xiàn)流程
讀萬卷書不如行萬里路,只學(xué)書上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SSM+jsp+mysql+maven實(shí)現(xiàn)一個在線美食網(wǎng)站系統(tǒng),大家可以在過程中查缺補(bǔ)漏,提升水平2021-11-11

