您好,登錄后才能下訂單哦!
本篇內(nèi)容主要講解“怎么理解ThreadLocal的實(shí)現(xiàn)機(jī)制”,感興趣的朋友不妨來(lái)看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來(lái)帶大家學(xué)習(xí)“怎么理解ThreadLocal的實(shí)現(xiàn)機(jī)制”吧!
下面的例子演示了 ThreadLocal 的典型應(yīng)用場(chǎng)景。在 jdk 1.8 之前,如果我們希望對(duì)日期和時(shí)間進(jìn)行格式化操作,則需要使用 SimpleDateFormat 類,而我們知道它是是線程不安全的,在多線程并發(fā)執(zhí)行時(shí)會(huì)出現(xiàn)一些奇怪的問(wèn)題。對(duì)于該類使用的最佳實(shí)踐則是采用 ThreadLocal 進(jìn)行包裝,以保證每個(gè)線程都有一份屬于自己的 SimpleDateFormat 對(duì)象,如下所示:
ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } };
那么 ThreadLocal 是怎么做到讓修飾的對(duì)象能夠在每個(gè)線程中各自持有一份呢?我們先來(lái)從整體的角度簡(jiǎn)單概括一下。
在 ThreadLocal 中定義了一個(gè)靜態(tài)內(nèi)部類 ThreadLocalMap,可以將其理解為一個(gè)特有的 Map 類型,而在 Thread 類中聲明了一個(gè) ThreadLocalMap 類型的 threadLocals 屬性。針對(duì)每個(gè) Thread 對(duì)象,也就是每個(gè)線程來(lái)說(shuō)都包含了一個(gè) ThreadLocalMap 對(duì)象,即每個(gè)線程都有一個(gè)屬于自己的內(nèi)存數(shù)據(jù)庫(kù),而數(shù)據(jù)庫(kù)中存儲(chǔ)的就是我們用 ThreadLocal 修飾的對(duì)象。整個(gè)過(guò)程還是有點(diǎn)繞的,可以借助下面這幅圖進(jìn)行理解:
這里的 key 就是對(duì)應(yīng)的 ThreadLocal 對(duì)象自身,而 value 就是 ThreadLocal 修飾的屬性值。當(dāng)希望獲取該對(duì)象時(shí),我們首先需要拿到當(dāng)前線程對(duì)應(yīng)的 Thread 對(duì)象,然后獲取到該對(duì)象對(duì)應(yīng)的 threadLocals 屬性,也就拿到了線程私有的內(nèi)存數(shù)據(jù)庫(kù),最后以 ThreadLocal 對(duì)象為 key 獲取到其修飾的目標(biāo)值。
接下來(lái)看一下相應(yīng)的源碼實(shí)現(xiàn),首先來(lái)看一下內(nèi)部定義的 ThreadLocalMap 靜態(tài)內(nèi)部類:
static class ThreadLocalMap { // 弱引用的key,繼承自 WeakReference static class Entry extends WeakReference<ThreadLocal<?>> { /** ThreadLocal 修飾的對(duì)象 */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } /** 初始化大小,必須是二次冪 */ private static final int INITIAL_CAPACITY = 16; /** 承載鍵值對(duì)的表,長(zhǎng)度必須是二次冪 */ private Entry[] table; /** 記錄鍵值對(duì)表的大小 */ 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),可以簡(jiǎn)單將其理解為一般的 Map,用作鍵值存儲(chǔ)的內(nèi)存數(shù)據(jù)庫(kù),至于為什么要專門實(shí)現(xiàn)而不是復(fù)用已有的 HashMap,我們?cè)诤竺孢M(jìn)行說(shuō)明。
了解了 ThreadLocalMap 的定義,我們?cè)賮?lái)看一下 ThreadLocal 的實(shí)現(xiàn)。對(duì)于 ThreadLocal 來(lái)說(shuō),對(duì)外暴露的方法主要有 get、set,以及 remove 三個(gè),下面逐一展開(kāi)分析。
與一般的 Map 取值操作不同,這里的 ThreadLocal#get
方法并沒(méi)有要求提供查詢的 key,也正如前面所說(shuō)的,這里的 key 就是調(diào)用 ThreadLocal#get
方法的 ThreadLocal 對(duì)象自身:
public T get() { // 獲取當(dāng)前線程對(duì)象 Thread t = Thread.currentThread(); // 獲取當(dāng)前線程對(duì)象的 threadLocals 屬性 ThreadLocalMap map = getMap(t); if (map != null) { // 以 ThreadLocal 對(duì)象為 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)前線程對(duì)應(yīng)的內(nèi)存數(shù)據(jù)庫(kù) map 對(duì)象還未創(chuàng)建,則會(huì)調(diào)用 ThreadLocal#setInitialValue
方法執(zhí)行創(chuàng)建,如果在構(gòu)造 ThreadLocal 對(duì)象時(shí)覆蓋實(shí)現(xiàn)了 ThreadLocal#initialValue
方法,則會(huì)調(diào)用該方法獲取構(gòu)造的初始化值并記錄到創(chuàng)建的 map 對(duì)象中:
private T setInitialValue() { // 調(diào)用模板方法 initialValue 獲取指定的初始值 T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) // 以當(dāng)前 ThreadLocal 對(duì)象為 key 記錄初始值 map.set(this, value); else // 創(chuàng)建 map 并記錄初始值 createMap(t, value); return value; }
再來(lái)看一下 ThreadLocal#set
方法,因?yàn)?key 就是當(dāng)前 ThreadLocal 對(duì)象,所以 ThreadLocal#set
方法也不需要指定 key:
public void set(T value) { // 獲取當(dāng)前線程對(duì)象 Thread t = Thread.currentThread(); // 獲取當(dāng)前線程對(duì)象的 threadLocals 屬性 ThreadLocalMap map = getMap(t); if (map != null) // 以當(dāng)前 ThreadLocal 對(duì)象為 key 記錄線程私有值 map.set(this, value); else createMap(t, value); }
和 ThreadLocal#get
方法的流程大致一樣,都是操作當(dāng)前線程私有的內(nèi)存數(shù)據(jù)庫(kù) ThreadLocalMap,并記錄目標(biāo)值。
方法 ThreadLocal#remove
以當(dāng)前 ThreadLocal 對(duì)象為 key,從當(dāng)前線程內(nèi)存數(shù)據(jù)庫(kù) ThreadLocalMap 中刪除目標(biāo)值,具體邏輯比較簡(jiǎn)單:
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) // 以當(dāng)前 ThreadLocal 對(duì)象為 key m.remove(this); }
ThreadLocal 對(duì)外暴露的功能雖然有點(diǎn)小神奇,但是具體對(duì)應(yīng)到內(nèi)部實(shí)現(xiàn)并沒(méi)有什么復(fù)雜的邏輯。如果我們把每個(gè)線程持有的專屬 ThreadLocalMap 對(duì)象理解為當(dāng)前線程的私有數(shù)據(jù)庫(kù),那么也就不難理解 ThreadLocal 的運(yùn)行機(jī)制。每個(gè)線程自己維護(hù)自己的數(shù)據(jù),彼此相互隔離,不存在競(jìng)爭(zhēng),也就沒(méi)有線程安全問(wèn)題可言。
雖然對(duì)于每個(gè)線程來(lái)說(shuō)數(shù)據(jù)是隔離的,但這也不表示任何對(duì)象丟到 ThreadLocal 中就萬(wàn)事大吉了,思考一下下面幾種情況:
如果記錄在 ThreadLocal 中的是一個(gè)線程共享的外部對(duì)象呢?
引入線程池,情況又會(huì)有什么變化?
如果 ThreadLocal 被 static 關(guān)鍵字修飾呢?
先來(lái)看 第 1 個(gè)問(wèn)題,如果我們記錄的是一個(gè)外部線程共享的對(duì)象,雖然我們以當(dāng)前線程私有的 ThreadLocal 對(duì)象作為 key 對(duì)其進(jìn)行了存儲(chǔ),但是惡魔終究是惡魔,共享的本質(zhì)并不會(huì)因此而改變,這種情況下的訪問(wèn)還是需要進(jìn)行同步控制,最好的方法就是從源頭屏蔽掉這類問(wèn)題。我們來(lái)舉個(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è)線程之間被訪問(wèn),如果不加控制則會(huì)存在線程安全問(wèn)題。
再來(lái)看 第 2 個(gè)問(wèn)題,相對(duì)問(wèn)題 1 來(lái)說(shuō)引入線程池就更加可怕,因?yàn)榇蟛糠謺r(shí)候我們都不會(huì)意識(shí)到問(wèn)題的存在,直到代碼暴露出奇怪的現(xiàn)象。這一場(chǎng)景并沒(méi)有違背線程私有的本質(zhì),只是一個(gè)線程被復(fù)用來(lái)處理多個(gè)業(yè)務(wù),而這個(gè)被線程私有的對(duì)象也會(huì)在多個(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]
示例中,我用一個(gè)大小為 2 的線程池進(jìn)行了模擬,可以看到初始化方法被調(diào)用了兩次,所有線程的操作都是復(fù)用這兩個(gè)線程。
回憶一下前文所說(shuō)的,ThreadLocal 的本質(zhì)就是為每個(gè)線程維護(hù)一個(gè)線程私有的內(nèi)存數(shù)據(jù)庫(kù)來(lái)記錄線程私有的對(duì)象,但是在線程池情況下線程是會(huì)被復(fù)用的,也就是說(shuō)線程私有的內(nèi)存數(shù)據(jù)庫(kù)也會(huì)被復(fù)用,如果在一個(gè)線程被使用完準(zhǔn)備回放到線程池中之前,我們沒(méi)有對(duì)記錄在數(shù)據(jù)庫(kù)中的數(shù)據(jù)執(zhí)行清理,那么這部分?jǐn)?shù)據(jù)就會(huì)被下一個(gè)復(fù)用該線程的業(yè)務(wù)看到,從而間接的共享了該部分?jǐn)?shù)據(jù)。
最后我們?cè)賮?lái)看一下 第 3 個(gè)問(wèn)題,我們嘗試將 ThreadLocal 對(duì)象用 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 修飾并沒(méi)有引出什么問(wèn)題,實(shí)際上這也是很容易理解的,ThreadLocal 采用 static 修飾僅僅是讓數(shù)據(jù)庫(kù)中記錄的 key 是一樣的,但是每個(gè)線程的內(nèi)存數(shù)據(jù)庫(kù)還是私有的,并沒(méi)有被共享,就像不同的公司都有自己的用戶信息表,即使一些公司之間的用戶 ID 是一樣的,但是對(duì)應(yīng)的用戶數(shù)據(jù)卻是完全隔離的。
以上例子演示了一開(kāi)始拋出的 3 個(gè)問(wèn)題,其中問(wèn)題 1 和問(wèn)題 2 都是 ThreadLocal 使用過(guò)程中的小地雷。例子舉的不一定恰當(dāng),實(shí)際中可能也不一定會(huì)如示例中這樣去使用 ThreadLocal,主要還是為了傳達(dá)一些意識(shí)。如果明白了 ThreadLocal 的內(nèi)部實(shí)現(xiàn)細(xì)節(jié),就能夠很自然的繞過(guò)這些小地雷。
關(guān)于 ThreadLocal 導(dǎo)致內(nèi)存泄露的問(wèn)題,曾經(jīng)有一段時(shí)間在網(wǎng)上爭(zhēng)得沸沸揚(yáng)揚(yáng),那么到底會(huì)不會(huì)導(dǎo)致內(nèi)存泄露呢?這里先給出答案:
如果使用不恰當(dāng),存在內(nèi)存泄露的可能性。
我們來(lái)分析一下內(nèi)存泄露的條件和原因,在最開(kāi)始看 ThreadLocal 源碼的時(shí)候,我就有一個(gè)疑問(wèn),ThreadLocal 為什么要專門實(shí)現(xiàn) ThreadLocalMap,而不是采用已有的 HashMap 代替?
后來(lái)分析具體實(shí)現(xiàn)時(shí)看到執(zhí)行存儲(chǔ)時(shí)的 key 為當(dāng)前 ThreadLocal 對(duì)象,不需要專門指定 key 能夠在一定程度上簡(jiǎn)化使用,但這并不足以為此專門去實(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è)弱引用,我們知道弱引用對(duì)象擁有非常短暫的生命周期,在垃圾收集器線程掃描其所管轄的內(nèi)存區(qū)域過(guò)程中,一旦發(fā)現(xiàn)了弱引用對(duì)象,不管當(dāng)前內(nèi)存空間是否足夠都會(huì)回收它的內(nèi)存。也就是說(shuō)這樣的設(shè)計(jì)會(huì)很容易導(dǎo)致 ThreadLocal 對(duì)象被回收,線程所執(zhí)行任務(wù)的時(shí)間長(zhǎng)度是不固定的,這樣的設(shè)計(jì)能夠方便垃圾收集器回收線程私有的變量。
由此可以看出作者這樣設(shè)計(jì)的目的是為了防止內(nèi)存泄露,那怎么就變成了被很多文章所分析的是內(nèi)存泄漏的導(dǎo)火索呢?這些文章的共同觀點(diǎn)就是 key 被回收了,但是 value 是一個(gè)強(qiáng)引用沒(méi)有被回收,這些 value 就變成了一個(gè)個(gè)的僵尸。這樣的分析沒(méi)有錯(cuò),value 確實(shí)存在,且和線程是同生命周期的,但是如下策略可以保證盡量避免內(nèi)存泄露:
ThreadLocal 在每次執(zhí)行 get 和 set 操作的時(shí)候都會(huì)去清理 key 為 null 的 value 值。
value 與線程同生命周期,線程死亡之時(shí),也是 value 被 GC 之日。
策略 1 沒(méi)啥好說(shuō)的,看看源碼就知道,我們來(lái)舉例驗(yàn)證一下策略 2:
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 了,雖然第 1 次 GC 的時(shí)候沒(méi)有被回收,這也驗(yàn)證 value 和線程是同生命周期的,之所以示例中等待 60 秒是因?yàn)?Executors#newCachedThreadPool
中的線程默認(rèn)生命周期是 60 秒,如果生命周期內(nèi)該線程沒(méi)有被再次復(fù)用則會(huì)死亡,我們這里就是要等待線程死亡,一但線程死亡,value 也就被 GC 了。
所以 出現(xiàn)內(nèi)存泄露的前提必須是持有 value 的線程一直存活,這在使用線程池時(shí)是很正常的,在這種情況下 value 一直不會(huì)被 GC,因?yàn)榫€程對(duì)象與 value 之間維護(hù)的是強(qiáng)引用。此外就是 后續(xù)線程執(zhí)行的業(yè)務(wù)一直沒(méi)有調(diào)用 ThreadLocal 的 get 或 set 方法,導(dǎo)致不會(huì)主動(dòng)去刪除 key 為 null 的 value 對(duì)象,在滿足這兩個(gè)條件下 value 對(duì)象一直常駐內(nèi)存,所以存在內(nèi)存泄露的可能性。
那么我們應(yīng)該怎么避免呢?前面我們分析過(guò)線程池情況下使用 ThreadLocal 存在小地雷,這里的內(nèi)存泄露一般也都是發(fā)生在線程池的情況下,所以在使用 ThreadLocal 時(shí),對(duì)于不再有效的 value 主動(dòng)調(diào)用一下 remove 方法來(lái)進(jìn)行清除,從而消除隱患,這也算是最佳實(shí)踐吧。
InheritableThreadLocal 繼承自 ThreadLocal,實(shí)現(xiàn)上也比較簡(jiǎn)單(如下),那么 InheritableThreadLocal 與 ThreadLocal 到底有什么區(qū)別呢?
public class InheritableThreadLocal<T> extends ThreadLocal<T> { @Override protected T childValue(T parentValue) { return parentValue; } @Override ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } @Override void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } }
在開(kāi)始分析之前,我們先演示一個(gè) ThreadLocal 的案例,如下:
private static ThreadLocal<String> tl = new ThreadLocal<>(); public static void main(String[] args) { tl.set("zhenchao"); System.out.println("Main thread: " + tl.get()); Thread thread = new Thread(() -> System.out.println("Sub thread: " + tl.get())); thread.start(); }
運(yùn)行上述示例,輸出如下:
Main thread: zhenchao Sub thread: null
可以看出,子線程拿不到主線程設(shè)置的 ThreadLocal 變量,當(dāng)然這也是可以理解的,畢竟主線程和子線程之間仍然是兩個(gè)線程,但是在一些場(chǎng)景下我們希望對(duì)于主線程和子線程這種關(guān)系而言,ThreadLocal 變量能夠被繼承。這個(gè)時(shí)候就可以使用 InheritableThreadLocal 來(lái)實(shí)現(xiàn),對(duì)于上述示例而言,只需要將 ThreadLocal 改為 InheritableThreadLocal 即可,具體實(shí)現(xiàn)比較簡(jiǎn)單,讀者可以自己嘗試一下。
下面我們來(lái)分析一下 InheritableThreadLocal 是如何做到讓 ThreadLocal 變量在主線程和子線程之間進(jìn)行繼承的。由 InheritableThreadLocal 的實(shí)現(xiàn)來(lái)看,InheritableThreadLocal 使用了 inheritableThreadLocals 變量替換了 ThreadLocal 的 threadLocals 變量,而這兩個(gè)變量都是 ThreadLocalMap 類型。子線程在初始化時(shí)會(huì)判斷父線程的 inheritableThreadLocals 是否為 null,如果不為 null,則使用父類的 inheritableThreadLocals 變量初始化自己的 inheritableThreadLocals,實(shí)現(xiàn)如下(位于 Thread#init
方法中):
// 如果父線程的 inheritableThreadLocals 變量不為空,則復(fù)制給子線程 if (inheritThreadLocals && parent.inheritableThreadLocals != null) { this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); }
而 ThreadLocal#createInheritedMap
的實(shí)現(xiàn)如下:
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); } 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) { // 調(diào)用 InheritableThreadLocal 的 childValue 方法 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++; } } } }
方法 InheritableThreadLocal#childValue
的實(shí)現(xiàn)只是簡(jiǎn)單返回了父線程中的值,所以上述過(guò)程本質(zhì)上就是一個(gè)拷貝父線程中 ThreadLocal 變量值的過(guò)程。
到此,相信大家對(duì)“怎么理解ThreadLocal的實(shí)現(xiàn)機(jī)制”有了更深的了解,不妨來(lái)實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。