筆者所有文章第一時(shí)間發(fā)布于:
hhbbz的個(gè)人博客
Java 多線程類庫對于共享數(shù)據(jù)的讀寫控制主要采用鎖機(jī)制保證線程安全,本文所要探究的 ThreadLocal 則采用了一種完全不同的策略。ThreadLocal 不是用來解決共享數(shù)據(jù)的并發(fā)訪問問題的,它讓每個(gè)線程都將目標(biāo)數(shù)據(jù)復(fù)制一份作為線程私有,后續(xù)對于該數(shù)據(jù)的操作都是在各自私有的副本上進(jìn)行,線程之間彼此相互隔離,也就不存在競爭問題。
下面的例子演示了 ThreadLocal 的典型應(yīng)用場景,在 jdk 1.8 之前,如果我們希望對日期和時(shí)間進(jìn)行格式化操作,則需要使用 SimpleDateFormat 類,而我們知道它是是非線程安全的,在多線程并發(fā)執(zhí)行時(shí)會出現(xiàn)一些奇怪的問題,而對于該類使用的最佳實(shí)踐則是采用 ThreadLocal 進(jìn)行包裝,以保證每個(gè)線程都有一份屬于自己的 SimpleDateFormat 對象,如下所示:
ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
一. 線程安全機(jī)制
那么 ThreadLocal 是怎么做到讓修飾的對象能夠在每個(gè)線程中各持有一份呢?我們先來簡單的概括一下:在 ThreadLocal 中定義了一個(gè)靜態(tài)內(nèi)部類 ThreadLocalMap,可以將其理解為一個(gè)特有的 Map 類型,而在 Thread 類中聲明了一個(gè) ThreadLocalMap 類型的屬性 threadLocals,所以針對每個(gè) Thread 對象,也就是每個(gè)線程來說都包含了一個(gè) ThreadLocalMap 對象,即每個(gè)線程都有一個(gè)屬于自己的內(nèi)存數(shù)據(jù)庫,而數(shù)據(jù)庫中存儲的就是我們用 ThreadLocal 修飾的對象,這里的 key 就是對應(yīng)的 ThreadLocal 對象,而 value 就是我們記錄在 ThreadLocal 中的值。當(dāng)希望獲取該對象時(shí),我們首先需要拿到當(dāng)前線程對應(yīng)的 Thread 對象,然后獲取到該對象對應(yīng)的 threadLocals 屬性,也就拿到了線程私有的內(nèi)存數(shù)據(jù)庫,最后以 ThreadLocal 對象為 key 獲取到其修飾的目標(biāo)值。整個(gè)過程還是有點(diǎn)繞的,可以借助下面這幅圖進(jìn)行理解。

1.1 內(nèi)存數(shù)據(jù)庫 ThreadLocalMap
接下來看一下相應(yīng)的源碼實(shí)現(xiàn),首先來看一下內(nèi)部定義的 ThreadLocalMap 靜態(tài)內(nèi)部類:
static class ThreadLocalMap {
// 弱引用的key,繼承自 WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
/** 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; // Default to 0
// 構(gòu)造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
// 構(gòu)造方法
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
// 省略相應(yīng)的方法實(shí)現(xiàn)
}
ThreadLocalMap 是一個(gè)定制化的 Map 實(shí)現(xiàn),這里可以簡單將其理解為一般的 Map,用作鍵值存儲的內(nèi)存數(shù)據(jù)庫,至于為什么要專門實(shí)現(xiàn)而不是復(fù)用已有的 HashMap,我們在后面進(jìn)行說明。
1.2 ThreadLocal 方法實(shí)現(xiàn)
了解了 ThreadLocalMap 的定義,我們再來看一下 ThreadLocal 的實(shí)現(xiàn)。對于 ThreadLocal 來說,對外暴露的方法主要有 get、set,以及 remove 三個(gè),下面逐一來看:
- 獲取線程私有值:get()
與一般的 Map 取值操作不同,這里的 get() 并沒有要求提供查詢的 key,也正如前面所說的,這里的 key 就是調(diào)用 get() 方法的對象自身:
public T get() {
// 獲取當(dāng)前線程對象
Thread t = Thread.currentThread();
// 獲取當(dāng)前線程對象的 threadLocals 屬性
ThreadLocalMap map = getMap(t);
if (map != null) {
// 以 ThreadLocal 對象為 key 獲取目標(biāo)線程私有值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
如果當(dāng)前線程對應(yīng)的內(nèi)存數(shù)據(jù)庫 map 對象還未創(chuàng)建,則會調(diào)用 setInitialValue() 方法執(zhí)行創(chuàng)建,如果在構(gòu)造 ThreadLocal 對象時(shí)覆蓋實(shí)現(xiàn)了 initialValue() 方法,則會調(diào)用該方法獲取構(gòu)造的初始化值并記錄到創(chuàng)建的 map 對象中:
private T setInitialValue() {
// 調(diào)用模板方法 initialValue 獲取指定的初始值
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// 以當(dāng)前 ThreadLocal 對象為 key 記錄初始值
map.set(this, value);
else
// 創(chuàng)建 map 并記錄初始值
createMap(t, value);
return value;
}
- 添加線程私有值:set(T value)
再來看一下 set 方法,因?yàn)?key 就是當(dāng)前 ThreadLocal 對象,所以 set 方法也不需要指定 key:
public void set(T value) {
// 獲取當(dāng)前線程對象
Thread t = Thread.currentThread();
// 獲取當(dāng)前線程對象的 threadLocals 屬性
ThreadLocalMap map = getMap(t);
if (map != null)
// 以當(dāng)前 ThreadLocal 對象為 key 記錄線程私有值
map.set(this, value);
else
createMap(t, value);
}
和 get 方法的流程大致一樣,都是操作當(dāng)前線程私有的內(nèi)存數(shù)據(jù)庫 ThreadLocalMap,并記錄目標(biāo)值。
- 刪除線程私有值:remove()
remove 方法以當(dāng)前 ThreadLocal 為 key,從當(dāng)前線程內(nèi)存數(shù)據(jù)庫 ThreadLocalMap 中刪除目標(biāo)值,具體邏輯比較簡單:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 以當(dāng)前 ThreadLocal 對象為 key
m.remove(this);
}
ThreadLocal 對外暴露的功能雖然有點(diǎn)小神奇,但是具體對應(yīng)到內(nèi)部實(shí)現(xiàn)并沒有什么復(fù)雜的邏輯,如果我們把每個(gè)線程持有的專屬 ThreadLocalMap 對象理解為當(dāng)前線程的私有數(shù)據(jù)庫,那么也就不難理解 ThreadLocal 的運(yùn)行機(jī)制,每個(gè)線程自己維護(hù)自己的數(shù)據(jù),彼此相互隔離,不存在競爭,也就沒有線程安全問題可言。
二. 真的就高枕無憂了嗎?
雖然對于每個(gè)線程來說數(shù)據(jù)是隔離的,但這也不表示任何對象丟到 ThreadLocal 中就萬事大吉了,思考一下下面幾種情況:
1. 如果記錄在 ThreadLocal 中的是一個(gè)線程共享的外部對象呢?
2. 引入線程池,情況又會有什么變化?
3. 如果 ThreadLocal 被 static 關(guān)鍵字修飾呢?
先來看 第一個(gè)問題 ,如果我們記錄的是一個(gè)外部線程共享的對象,雖然我們以當(dāng)前線程私有的 ThreadLocal 對象作為 key 對其進(jìn)行了存儲,但是惡魔終究是惡魔,共享的本質(zhì)并不會因此而改變,這種情況下的訪問還是需要進(jìn)行同步控制,最好的方法就是從源頭屏蔽掉這類問題。我們來舉個(gè)例子:
public class ThreadLocalWithSharedInstance implements Runnable {
// list 是一個(gè)事實(shí)共享的實(shí)例,即使被 ThreadLocal 修飾
private static List<String> list = new ArrayList<>();
private ThreadLocal<List<String>> threadLocal = ThreadLocal.withInitial(() -> list);
@Override
public void run() {
for (int i = 0; i < 5; i++) {
List<String> li = threadLocal.get();
li.add(Thread.currentThread().getName() + "_" + RandomUtils.nextInt(0, 10));
threadLocal.set(li);
}
System.out.println("[Thread-" + Thread.currentThread().getName() + "], list=" + threadLocal.get());
}
public static void main(String[] args) throws Exception {
Thread ta = new Thread(new ThreadLocalWithSharedInstance(), "a");
Thread tb = new Thread(new ThreadLocalWithSharedInstance(), "b");
Thread tc = new Thread(new ThreadLocalWithSharedInstance(), "c");
ta.start(); ta.join();
tb.start(); tb.join();
tc.start(); tc.join();
}
}
以上程序最終的輸出如下:
[Thread-a], list=[a_2, a_7, a_4, a_5, a_7]
[Thread-b], list=[a_2, a_7, a_4, a_5, a_7, b_3, b_3, b_4, b_7, b_7]
[Thread-c], list=[a_2, a_7, a_4, a_5, a_7, b_3, b_3, b_4, b_7, b_7, c_8, c_3, c_4, c_7, c_5]
可以看到雖然使用了 ThreadLocal 修飾,但是 list 還是以共享的方式在多個(gè)線程之間被訪問,如果不加同步控制,則會存在線程安全問題。
再來看 第二個(gè)問題 ,相對問題一來說引入線程池就更加可怕,因?yàn)榇蟛糠謺r(shí)候我們都不會意識到問題的存在,直到代碼暴露出奇怪的現(xiàn)象,這個(gè)時(shí)候并沒有違背線程私有的本質(zhì),只是一個(gè)線程被復(fù)用來處理多個(gè)業(yè)務(wù),而這個(gè)被線程私有的對象也會在多個(gè)業(yè)務(wù)之間被 “共享”。例如:
public class ThreadLocalWithThreadPool implements Callable<Boolean> {
private static final int NCPU = Runtime.getRuntime().availableProcessors();
private ThreadLocal<List<String>> threadLocal = ThreadLocal.withInitial(() -> {
System.out.println("thread-" + Thread.currentThread().getId() + " init thread local");
return new ArrayList<>();
});
@Override
public Boolean call() throws Exception {
for (int i = 0; i < 5; i++) {
List<String> li = threadLocal.get();
li.add(Thread.currentThread().getId() + "_" + RandomUtils.nextInt(0, 10));
threadLocal.set(li);
}
System.out.println("[Thread-" + Thread.currentThread().getId() + "], list=" + threadLocal.get());
return true;
}
public static void main(String[] args) throws Exception {
System.out.println("cpu core size : " + NCPU);
List<Callable<Boolean>> tasks = new ArrayList<>(NCPU * 2);
ThreadLocalWithThreadPool tl = new ThreadLocalWithThreadPool();
for (int i = 0; i < NCPU * 2; i++) {
tasks.add(tl);
}
ExecutorService es = Executors.newFixedThreadPool(2);
List<Future<Boolean>> futures = es.invokeAll(tasks);
for (final Future<Boolean> future : futures) {
future.get();
}
es.shutdown();
}
}
以上程序的最終輸出如下:
cpu core size : 8
thread-12 init thread local
thread-11 init thread local
[Thread-12], list=[12_8, 12_8, 12_4, 12_0, 12_1]
[Thread-11], list=[11_3, 11_3, 11_4, 11_8, 11_4]
[Thread-12], list=[12_8, 12_8, 12_4, 12_0, 12_1, 12_6, 12_7, 12_8, 12_8, 12_8]
[Thread-11], list=[11_3, 11_3, 11_4, 11_8, 11_4, 11_0, 11_2, 11_1, 11_7, 11_9]
[Thread-12], list=[12_8, 12_8, 12_4, 12_0, 12_1, 12_6, 12_7, 12_8, 12_8, 12_8, 12_8, 12_2, 12_8, 12_0, 12_6]
[Thread-11], list=[11_3, 11_3, 11_4, 11_8, 11_4, 11_0, 11_2, 11_1, 11_7, 11_9, 11_0, 11_6, 11_1, 11_2, 11_9]
[Thread-12], list=[12_8, 12_8, 12_4, 12_0, 12_1, 12_6, 12_7, 12_8, 12_8, 12_8, 12_8, 12_2, 12_8, 12_0, 12_6, 12_6, 12_3, 12_3, 12_1, 12_1]
[Thread-11], list=[11_3, 11_3, 11_4, 11_8, 11_4, 11_0, 11_2, 11_1, 11_7, 11_9, 11_0, 11_6, 11_1, 11_2, 11_9, 11_7, 11_5, 11_0, 11_6, 11_9]
[Thread-12], list=[12_8, 12_8, 12_4, 12_0, 12_1, 12_6, 12_7, 12_8, 12_8, 12_8, 12_8, 12_2, 12_8, 12_0, 12_6, 12_6, 12_3, 12_3, 12_1, 12_1, 12_0, 12_0, 12_1, 12_9, 12_5]
[Thread-11], list=[11_3, 11_3, 11_4, 11_8, 11_4, 11_0, 11_2, 11_1, 11_7, 11_9, 11_0, 11_6, 11_1, 11_2, 11_9, 11_7, 11_5, 11_0, 11_6, 11_9, 11_2, 11_7, 11_0, 11_8, 11_0]
[Thread-12], list=[12_8, 12_8, 12_4, 12_0, 12_1, 12_6, 12_7, 12_8, 12_8, 12_8, 12_8, 12_2, 12_8, 12_0, 12_6, 12_6, 12_3, 12_3, 12_1, 12_1, 12_0, 12_0, 12_1, 12_9, 12_5, 12_3, 12_6, 12_6, 12_0, 12_9]
[Thread-11], list=[11_3, 11_3, 11_4, 11_8, 11_4, 11_0, 11_2, 11_1, 11_7, 11_9, 11_0, 11_6, 11_1, 11_2, 11_9, 11_7, 11_5, 11_0, 11_6, 11_9, 11_2, 11_7, 11_0, 11_8, 11_0, 11_0, 11_9, 11_2, 11_7, 11_2]
[Thread-12], list=[12_8, 12_8, 12_4, 12_0, 12_1, 12_6, 12_7, 12_8, 12_8, 12_8, 12_8, 12_2, 12_8, 12_0, 12_6, 12_6, 12_3, 12_3, 12_1, 12_1, 12_0, 12_0, 12_1, 12_9, 12_5, 12_3, 12_6, 12_6, 12_0, 12_9, 12_5, 12_7, 12_7, 12_9, 12_7]
[Thread-11], list=[11_3, 11_3, 11_4, 11_8, 11_4, 11_0, 11_2, 11_1, 11_7, 11_9, 11_0, 11_6, 11_1, 11_2, 11_9, 11_7, 11_5, 11_0, 11_6, 11_9, 11_2, 11_7, 11_0, 11_8, 11_0, 11_0, 11_9, 11_2, 11_7, 11_2, 11_4, 11_9, 11_7, 11_5, 11_5]
[Thread-12], list=[12_8, 12_8, 12_4, 12_0, 12_1, 12_6, 12_7, 12_8, 12_8, 12_8, 12_8, 12_2, 12_8, 12_0, 12_6, 12_6, 12_3, 12_3, 12_1, 12_1, 12_0, 12_0, 12_1, 12_9, 12_5, 12_3, 12_6, 12_6, 12_0, 12_9, 12_5, 12_7, 12_7, 12_9, 12_7, 12_6, 12_1, 12_7, 12_8, 12_7]
[Thread-11], list=[11_3, 11_3, 11_4, 11_8, 11_4, 11_0, 11_2, 11_1, 11_7, 11_9, 11_0, 11_6, 11_1, 11_2, 11_9, 11_7, 11_5, 11_0, 11_6, 11_9, 11_2, 11_7, 11_0, 11_8, 11_0, 11_0, 11_9, 11_2, 11_7, 11_2, 11_4, 11_9, 11_7, 11_5, 11_5, 11_8, 11_5, 11_0, 11_2, 11_2]
在我的 8 核處理器上,我用一個(gè)大小為 2 的線程池進(jìn)行了模擬,可以看到初始化方法被調(diào)用了兩次,所有線程的操作都是復(fù)用這兩個(gè)線程?;貞浺幌虑拔乃f的,ThreadLocal 的本質(zhì)就是每個(gè)線程維護(hù)一個(gè)線程私有的內(nèi)存數(shù)據(jù)庫來記錄線程私有的對象,但是在線程池情況下線程是會被復(fù)用的,也就是說線程私有的內(nèi)存數(shù)據(jù)庫也會被復(fù)用,如果在一個(gè)線程被使用完準(zhǔn)備回放到線程池中之前,我們沒有對記錄在數(shù)據(jù)庫中的數(shù)據(jù)執(zhí)行清理,那么這部分?jǐn)?shù)據(jù)就會被下一個(gè)復(fù)用該線程的業(yè)務(wù)看到,從而間接的共享了該部分?jǐn)?shù)據(jù)。
最后我們再來看一下 第三個(gè)問題 ,我們嘗試將 ThreadLocal 對象用 static 關(guān)鍵字進(jìn)行修飾:
public class ThreadLocalWithStaticEmbellish implements Runnable {
private static final int NCPU = Runtime.getRuntime().availableProcessors();
private static ThreadLocal<List<String>> threadLocal = ThreadLocal.withInitial(() -> {
System.out.println("thread-" + Thread.currentThread().getName() + " init thread local");
return new ArrayList<>();
});
@Override
public void run() {
for (int i = 0; i < 5; i++) {
List<String> li = threadLocal.get();
li.add(Thread.currentThread().getId() + "_" + RandomUtils.nextInt(0, 10));
threadLocal.set(li);
}
System.out.println("[Thread-" + Thread.currentThread().getName() + "], list=" + threadLocal.get());
}
public static void main(String[] args) throws Exception {
ThreadLocalWithStaticEmbellish tl = new ThreadLocalWithStaticEmbellish();
for (int i = 0; i < NCPU + 1; i++) {
Thread thread = new Thread(tl, String.valueOf((char) (i + 97)));
thread.start(); thread.join();
}
}
}
以上程序的最終輸出如下:
thread-a init thread local
[Thread-a], list=[11_4, 11_4, 11_4, 11_8, 11_0]
thread-b init thread local
[Thread-b], list=[12_0, 12_9, 12_0, 12_3, 12_3]
thread-c init thread local
[Thread-c], list=[13_6, 13_7, 13_5, 13_2, 13_0]
thread-d init thread local
[Thread-d], list=[14_1, 14_5, 14_5, 14_9, 14_2]
thread-e init thread local
[Thread-e], list=[15_4, 15_2, 15_6, 15_0, 15_8]
thread-f init thread local
[Thread-f], list=[16_7, 16_3, 16_8, 16_0, 16_0]
thread-g init thread local
[Thread-g], list=[17_6, 17_3, 17_8, 17_7, 17_1]
thread-h init thread local
[Thread-h], list=[18_0, 18_4, 18_5, 18_9, 18_3]
thread-i init thread local
[Thread-i], list=[19_7, 19_3, 19_7, 19_2, 19_0]
由程序運(yùn)行結(jié)果可以看到 static 修飾并沒有引出什么問題,實(shí)際上這也是很容易理解的,ThreadLocal 采用 static 修飾僅僅是讓數(shù)據(jù)庫中記錄的 key 是一樣的,但是每個(gè)線程的內(nèi)存數(shù)據(jù)庫還是私有的,并沒有被共享,就像不同的公司都有自己的用戶信息表,即使一些公司之間的用戶 ID 是一樣的,但是對應(yīng)的用戶數(shù)據(jù)卻是完全隔離的。
以上例子演示了一開始拋出的三個(gè)問題,其中問題一和問題二都是 ThreadLocal 使用過程中的小地雷。例子舉的不一定恰當(dāng),實(shí)際中可能也不一定會如示例中這樣去使用 ThreadLocal,主要還是為了傳達(dá)一些意識。如果明白了 ThreadLocal 的內(nèi)部實(shí)現(xiàn)細(xì)節(jié),就能夠很自然的繞過這些小地雷。
三. 真的會內(nèi)存泄露嗎?
關(guān)于 ThreadLocal 導(dǎo)致內(nèi)存泄露的問題,曾經(jīng)有一段時(shí)間在網(wǎng)上爭得沸沸揚(yáng)揚(yáng),那么到底會不會導(dǎo)致內(nèi)存泄露呢?這里先給出答案:
如果使用不恰當(dāng),存在內(nèi)存泄露的可能性。
我們來分析一下內(nèi)存泄露的條件和原因,在最開始看 ThreadLocal 源碼的時(shí)候,我就有一個(gè)疑問,ThreadLocal 為什么要專門實(shí)現(xiàn) ThreadLocalMap,而不是采用已有的 HashMap 代替?后來分析具體實(shí)現(xiàn)時(shí)看到執(zhí)行存儲時(shí)的 key 為當(dāng)前 ThreadLocal 對象,不需要專門指定 key 能夠在一定程度上簡化使用,但這并不足以為此專門去實(shí)現(xiàn) ThreadLocalMap。繼續(xù)閱讀我發(fā)現(xiàn) ThreadLocalMap 在實(shí)現(xiàn) Entry 的時(shí)候有些奇怪,居然繼承了 WeakReference:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
從而讓 key 成為一個(gè)弱引用,我們知道弱引用對象擁有非常短暫的生命周期,在垃圾收集器線程掃描其所管轄的內(nèi)存區(qū)域過程中,一旦發(fā)現(xiàn)了弱引用對象,不管當(dāng)前內(nèi)存空間是否足夠都會回收它的內(nèi)存。也就是說這樣的設(shè)計(jì)會很容易導(dǎo)致 ThreadLocal 對象被回收,線程所執(zhí)行任務(wù)的時(shí)間長度是不固定的,這樣的設(shè)計(jì)能夠方便垃圾收集器回收線程私有的變量。
所以作者這樣設(shè)計(jì)的目的是為了防止內(nèi)存泄露,那怎么就變成了被很多文章所分析的是內(nèi)存泄漏的導(dǎo)火索呢?這些文章的共同觀點(diǎn)就是 key 被回收了,但是 value 是一個(gè)強(qiáng)引用沒有被回收,這些 value 就變成了一個(gè)個(gè)的僵尸。這樣的分析沒有錯,value 確實(shí)存在,且和線程是同生命周期的,但是如下策略可以保證盡量避免內(nèi)存泄露:
1. ThreadLocal 在每次執(zhí)行 get 和 set 操作的時(shí)候都會去清理 key 為 null 的 value 值
2. value 與線程同生命周期,線程死亡之時(shí),也是 value 被 GC 之日
策略一沒啥好說的,看看源碼就知道,我們來舉例驗(yàn)證一下策略二:
public class ThreadLocalWithMemoryLeak implements Callable<Boolean> {
private class My50MB {
private byte[] buffer = new byte[50 * 1024 * 1024];
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("gc my 50 mb");
}
}
private class MyThreadLocal<T> extends ThreadLocal<T> {
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("gc my thread local");
}
}
private MyThreadLocal<My50MB> threadLocal = new MyThreadLocal<>();
@Override
public Boolean call() throws Exception {
System.out.println("Thread-" + Thread.currentThread().getId() + " is running");
threadLocal.set(new My50MB());
threadLocal = null;
return true;
}
public static void main(String[] args) throws Exception {
ExecutorService es = Executors.newCachedThreadPool();
Future<Boolean> future = es.submit(new ThreadLocalWithMemoryLeak());
future.get();
// gc my thread local
System.out.println("do gc");
System.gc();
TimeUnit.SECONDS.sleep(1);
// sleep 60s
System.out.println("sleep 60s");
TimeUnit.SECONDS.sleep(60);
// gc my 50 mb
System.out.println("do gc");
System.gc();
es.shutdown();
}
}
以上程序的最終輸出如下
Thread-11 is running
do gc
gc my thread local
sleep 60s
do gc
gc my 50 mb
可以看到 value 最終還是被 GC 了,雖然第一次 GC 的時(shí)候沒有被回收,這也驗(yàn)證 value 和線程是同生命周期的,之所以示例中等待 60 秒是因?yàn)?Executors.newCachedThreadPool() 中的線程默認(rèn)生命周期是 60 秒,如果生命周期內(nèi)該線程沒有被再次復(fù)用則會死亡,我們這里就是要等待線程死亡,一但線程死亡,value 也就被 GC 了。所以 出現(xiàn)內(nèi)存泄露的前提必須是持有 value 的線程一直存活 ,這在使用線程池時(shí)是很正常的,在這種情況下 value 一直不會被 GC,因?yàn)榫€程對象與 value 之間維護(hù)的是強(qiáng)引用。此外就是 后續(xù)線程執(zhí)行的業(yè)務(wù)一直沒有調(diào)用 ThreadLocal 的 get 或 set 方法,導(dǎo)致不會主動去刪除 key 為 null 的 value 對象 ,在滿足這兩個(gè)條件下 value 對象一直常駐內(nèi)存,所以存在內(nèi)存泄露的可能性。
那么我們應(yīng)該怎么避免呢?前面我們分析過線程池情況下使用 ThreadLocal 存在小地雷,這里的內(nèi)存泄露一般也都是發(fā)生在線程池的情況下,所以在使用 ThreadLocal 時(shí),對于不再有效的 value 主動調(diào)用一下 remove 方法來進(jìn)行清除,從而消除隱患,這也算是最佳實(shí)踐吧。