摘要:多線程類庫對于共享數(shù)據(jù)的讀寫控制主要采用鎖機制保證線程安全,本文所要探究的則采用了一種完全不同的策略。所以出現(xiàn)內(nèi)存泄露的前提必須是持有的線程一直存活,這在使用線程池時是很正常的,在這種情況下一直不會被,因為
Java 多線程類庫對于共享數(shù)據(jù)的讀寫控制主要采用鎖機制保證線程安全,本文所要探究的 ThreadLocal 則采用了一種完全不同的策略。ThreadLocal 不是用來解決共享數(shù)據(jù)的并發(fā)訪問問題的,它讓每個線程都將目標數(shù)據(jù)復制一份作為線程私有,后續(xù)對于該數(shù)據(jù)的操作都是在各自私有的副本上進行,線程之間彼此相互隔離,也就不存在競爭問題。
下面的例子演示了 ThreadLocal 的典型應用場景,在 jdk 1.8 之前,如果我們希望對日期和時間進行格式化操作,則需要使用 SimpleDateFormat 類,而我們知道它是是非線程安全的,在多線程并發(fā)執(zhí)行時會出現(xiàn)一些奇怪的問題,而對于該類使用的最佳實踐則是采用 ThreadLocal 進行包裝,以保證每個線程都有一份屬于自己的 SimpleDateFormat 對象,如下所示:
ThreadLocal一. 線程安全機制sdf = new ThreadLocal () { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } };
那么 ThreadLocal 是怎么做到讓修飾的對象能夠在每個線程中各持有一份呢?我們先來簡單的概括一下:在 ThreadLocal 中定義了一個靜態(tài)內(nèi)部類 ThreadLocalMap,可以將其理解為一個特有的 Map 類型,而在 Thread 類中聲明了一個 ThreadLocalMap 類型的屬性 threadLocals,所以針對每個 Thread 對象,也就是每個線程來說都包含了一個 ThreadLocalMap 對象,即每個線程都有一個屬于自己的內(nèi)存數(shù)據(jù)庫,而數(shù)據(jù)庫中存儲的就是我們用 ThreadLocal 修飾的對象,這里的 key 就是對應的 ThreadLocal 對象,而 value 就是我們記錄在 ThreadLocal 中的值。當希望獲取該對象時,我們首先需要拿到當前線程對應的 Thread 對象,然后獲取到該對象對應的 threadLocals 屬性,也就拿到了線程私有的內(nèi)存數(shù)據(jù)庫,最后以 ThreadLocal 對象為 key 獲取到其修飾的目標值。整個過程還是有點繞的,可以借助下面這幅圖進行理解。
1.1 內(nèi)存數(shù)據(jù)庫 ThreadLocalMap接下來看一下相應的源碼實現(xiàn),首先來看一下內(nèi)部定義的 ThreadLocalMap 靜態(tài)內(nèi)部類:
static class ThreadLocalMap { // 弱引用的key,繼承自 WeakReference 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; // 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
ThreadLocalMap 是一個定制化的 Map 實現(xiàn),這里可以簡單將其理解為一般的 Map,用作鍵值存儲的內(nèi)存數(shù)據(jù)庫,至于為什么要專門實現(xiàn)而不是復用已有的 HashMap,我們在后面進行說明。
1.2 ThreadLocal 方法實現(xiàn)了解了 ThreadLocalMap 的定義,我們再來看一下 ThreadLocal 的實現(xiàn)。對于 ThreadLocal 來說,對外暴露的方法主要有 get、set,以及 remove 三個,下面逐一來看:
獲取線程私有值:get()
與一般的 Map 取值操作不同,這里的 get() 并沒有要求提供查詢的 key,也正如前面所說的,這里的 key 就是調(diào)用 get() 方法的對象自身:
public T get() { // 獲取當前線程對象 Thread t = Thread.currentThread(); // 獲取當前線程對象的 threadLocals 屬性 ThreadLocalMap map = getMap(t); if (map != null) { // 以 ThreadLocal 對象為 key 獲取目標線程私有值 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
如果當前線程對應的內(nèi)存數(shù)據(jù)庫 map 對象還未創(chuàng)建,則會調(diào)用 setInitialValue() 方法執(zhí)行創(chuàng)建,如果在構(gòu)造 ThreadLocal 對象時覆蓋實現(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) // 以當前 ThreadLocal 對象為 key 記錄初始值 map.set(this, value); else // 創(chuàng)建 map 并記錄初始值 createMap(t, value); return value; }
添加線程私有值:set(T value)
再來看一下 set 方法,因為 key 就是當前 ThreadLocal 對象,所以 set 方法也不需要指定 key:
public void set(T value) { // 獲取當前線程對象 Thread t = Thread.currentThread(); // 獲取當前線程對象的 threadLocals 屬性 ThreadLocalMap map = getMap(t); if (map != null) // 以當前 ThreadLocal 對象為 key 記錄線程私有值 map.set(this, value); else createMap(t, value); }
和 get 方法的流程大致一樣,都是操作當前線程私有的內(nèi)存數(shù)據(jù)庫 ThreadLocalMap,并記錄目標值。
刪除線程私有值:remove()
remove 方法以當前 ThreadLocal 為 key,從當前線程內(nèi)存數(shù)據(jù)庫 ThreadLocalMap 中刪除目標值,具體邏輯比較簡單:
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) // 以當前 ThreadLocal 對象為 key m.remove(this); }
ThreadLocal 對外暴露的功能雖然有點小神奇,但是具體對應到內(nèi)部實現(xiàn)并沒有什么復雜的邏輯,如果我們把每個線程持有的專屬 ThreadLocalMap 對象理解為當前線程的私有數(shù)據(jù)庫,那么也就不難理解 ThreadLocal 的運行機制,每個線程自己維護自己的數(shù)據(jù),彼此相互隔離,不存在競爭,也就沒有線程安全問題可言。
二. 真的就高枕無憂了嗎?雖然對于每個線程來說數(shù)據(jù)是隔離的,但這也不表示任何對象丟到 ThreadLocal 中就萬事大吉了,思考一下下面幾種情況:
如果記錄在 ThreadLocal 中的是一個線程共享的外部對象呢?
引入線程池,情況又會有什么變化?
如果 ThreadLocal 被 static 關(guān)鍵字修飾呢?
先來看 第一個問題 ,如果我們記錄的是一個外部線程共享的對象,雖然我們以當前線程私有的 ThreadLocal 對象作為 key 對其進行了存儲,但是惡魔終究是惡魔,共享的本質(zhì)并不會因此而改變,這種情況下的訪問還是需要進行同步控制,最好的方法就是從源頭屏蔽掉這類問題。我們來舉個例子:
public class ThreadLocalWithSharedInstance implements Runnable { // list 是一個事實共享的實例,即使被 ThreadLocal 修飾 private static Listlist = new ArrayList<>(); private ThreadLocal > threadLocal = ThreadLocal.withInitial(() -> list); @Override public void run() { for (int i = 0; i < 5; i++) { List
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 還是以共享的方式在多個線程之間被訪問,如果不加同步控制,則會存在線程安全問題。
再來看 第二個問題 ,相對問題一來說引入線程池就更加可怕,因為大部分時候我們都不會意識到問題的存在,直到代碼暴露出奇怪的現(xiàn)象,這個時候并沒有違背線程私有的本質(zhì),只是一個線程被復用來處理多個業(yè)務,而這個被線程私有的對象也會在多個業(yè)務之間被 “共享”。例如:
public class ThreadLocalWithThreadPool implements Callable{ private static final int NCPU = Runtime.getRuntime().availableProcessors(); private ThreadLocal > 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
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 > 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 > futures = es.invokeAll(tasks); for (final Future 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 核處理器上,我用一個大小為 2 的線程池進行了模擬,可以看到初始化方法被調(diào)用了兩次,所有線程的操作都是復用這兩個線程。回憶一下前文所說的,ThreadLocal 的本質(zhì)就是每個線程維護一個線程私有的內(nèi)存數(shù)據(jù)庫來記錄線程私有的對象,但是在線程池情況下線程是會被復用的,也就是說線程私有的內(nèi)存數(shù)據(jù)庫也會被復用,如果在一個線程被使用完準備回放到線程池中之前,我們沒有對記錄在數(shù)據(jù)庫中的數(shù)據(jù)執(zhí)行清理,那么這部分數(shù)據(jù)就會被下一個復用該線程的業(yè)務看到,從而間接的共享了該部分數(shù)據(jù)(哈哈,你的筆記本電腦在送人之前一定要對硬盤執(zhí)行多次格式化,不然冠希哥會對你微笑哦)。
最后我們再來看一下 第三個問題 ,我們嘗試將 ThreadLocal 對象用 static 關(guān)鍵字進行修飾:
public class ThreadLocalWithStaticEmbellish implements Runnable { private static final int NCPU = Runtime.getRuntime().availableProcessors(); private static ThreadLocal> 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
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]
由程序運行結(jié)果可以看到 static 修飾并沒有引出什么問題,實際上這也是很容易理解的,ThreadLocal 采用 static 修飾僅僅是讓數(shù)據(jù)庫中記錄的 key 是一樣的,但是每個線程的內(nèi)存數(shù)據(jù)庫還是私有的,并沒有被共享,就像不同的公司都有自己的用戶信息表,即使一些公司之間的用戶 ID 是一樣的,但是對應的用戶數(shù)據(jù)卻是完全隔離的。
以上例子演示了一開始拋出的三個問題,其中問題一和問題二都是 ThreadLocal 使用過程中的小地雷。例子舉的不一定恰當,實際中可能也不一定會如示例中這樣去使用 ThreadLocal,主要還是為了傳達一些意識。如果明白了 ThreadLocal 的內(nèi)部實現(xiàn)細節(jié),就能夠很自然的繞過這些小地雷。
三. 真的會內(nèi)存泄露嗎?關(guān)于 ThreadLocal 導致內(nèi)存泄露的問題,曾經(jīng)有一段時間在網(wǎng)上爭得沸沸揚揚,那么到底會不會導致內(nèi)存泄露呢?這里先給出答案:
如果使用不恰當,存在內(nèi)存泄露的可能性。
我們來分析一下內(nèi)存泄露的條件和原因,在最開始看 ThreadLocal 源碼的時候,我就有一個疑問,__ThreadLocal 為什么要專門實現(xiàn) ThreadLocalMap,而不是采用已有的 HashMap 代替__?后來分析具體實現(xiàn)時看到執(zhí)行存儲時的 key 為當前 ThreadLocal 對象,不需要專門指定 key 能夠在一定程度上簡化使用,但這并不足以為此專門去實現(xiàn) ThreadLocalMap。繼續(xù)閱讀我發(fā)現(xiàn) ThreadLocalMap 在實現(xiàn) Entry 的時候有些奇怪,居然繼承了 WeakReference:
static class Entry extends WeakReference> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal> k, Object v) { super(k); value = v; } }
從而讓 key 成為一個弱引用,我們知道弱引用對象擁有非常短暫的生命周期,在垃圾收集器線程掃描其所管轄的內(nèi)存區(qū)域過程中,一旦發(fā)現(xiàn)了弱引用對象,不管當前內(nèi)存空間是否足夠都會回收它的內(nèi)存。也就是說這樣的設計會很容易導致 ThreadLocal 對象被回收,線程所執(zhí)行任務的時間長度是不固定的,這樣的設計能夠方便垃圾收集器回收線程私有的變量。
所以作者這樣設計的目的是為了防止內(nèi)存泄露,那怎么就變成了被很多文章所分析的是內(nèi)存泄漏的導火索呢?這些文章的共同觀點就是 key 被回收了,但是 value 是一個強引用沒有被回收,這些 value 就變成了一個個的僵尸。這樣的分析沒有錯,value 確實存在,且和線程是同生命周期的,但是如下策略可以保證盡量避免內(nèi)存泄露:
ThreadLocal 在每次執(zhí)行 get 和 set 操作的時候都會去清理 key 為 null 的 value 值
value 與線程同生命周期,線程死亡之時,也是 value 被 GC 之日
策略一沒啥好說的,看看源碼就知道,我們來舉例驗證一下策略二:
public class ThreadLocalWithMemoryLeak implements Callable{ 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 extends ThreadLocal { @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("gc my thread local"); } } private MyThreadLocal 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 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 的時候沒有被回收,這也驗證 value 和線程是同生命周期的,之所以示例中等待 60 秒是因為 Executors.newCachedThreadPool() 中的線程默認生命周期是 60 秒,如果生命周期內(nèi)該線程沒有被再次復用則會死亡,我們這里就是要等待線程死亡,一但線程死亡,value 也就被 GC 了。所以 出現(xiàn)內(nèi)存泄露的前提必須是持有 value 的線程一直存活 ,這在使用線程池時是很正常的,在這種情況下 value 一直不會被 GC,因為線程對象與 value 之間維護的是強引用。此外就是 后續(xù)線程執(zhí)行的業(yè)務一直沒有調(diào)用 ThreadLocal 的 get 或 set 方法,導致不會主動去刪除 key 為 null 的 value 對象 ,在滿足這兩個條件下 value 對象一直常駐內(nèi)存,所以存在內(nèi)存泄露的可能性。
那么我們應該怎么避免呢?前面我們分析過線程池情況下使用 ThreadLocal 存在小地雷,這里的內(nèi)存泄露一般也都是發(fā)生在線程池的情況下,所以在使用 ThreadLocal 時,對于不再有效的 value 主動調(diào)用一下 remove 方法來進行清除,從而消除隱患,這也算是最佳實踐吧。
本文最先發(fā)布于 “ 指間數(shù)據(jù) ” 公眾號,微信掃描下方二維碼進行關(guān)注,第一時間獲取高質(zhì)量的技術(shù)類文章。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/69026.html
摘要:提供了線程安全的共享對象,在編寫多線程代碼時,可把不安全的整個變量封裝進,或者把該對象與線程相關(guān)的狀態(tài)使用保存并不能替代同步機制,兩者面向的問題領(lǐng)域不同。 ThreadLocal類 使用ThreadLocal類可以簡化多線程編程時的并發(fā)訪問,使用這個工具類可以很簡捷地隔離多線程程序的競爭資源。Java5之后,為ThreadLocal類增加了泛型支持,即ThreadLocal Threa...
摘要:當某個不應該發(fā)布的對象被發(fā)布時,這種情況被稱為逸出。線程安全共享線程安全的對象在其內(nèi)部實現(xiàn)同步,因此多線程可以通過對象的公有接口來進行訪問而不需要進一步的同步。 前言 本系列博客是對《Java并發(fā)編程實戰(zhàn)》的一點總結(jié),本篇主要講解以下幾個內(nèi)容,內(nèi)容會比較枯燥??赡艽蠹铱礃祟}不能能直觀的感受出到底什么意思,這就是專業(yè)術(shù)語,哈哈,解釋下,術(shù)語(terminology)是在特定學科領(lǐng)域用...
摘要:當多個線程訪問實例時,每個線程維護提供的獨立的變量副本。而則從另一個角度來解決多線程的并發(fā)訪問。在執(zhí)行同步代碼塊的過程中,遇到異常而導致線程終止。在執(zhí)行同步代碼塊的過程中,其他線程執(zhí)行了當前對象的方法,當前線程被暫停,但不會釋放鎖。 一、Thread.start()與Thread.run()的區(qū)別通過調(diào)用Thread類的start()方法來啟動一個線程,這時此線程是處于就緒狀態(tài),并沒有...
摘要:線程安全問題都是由全局變量及靜態(tài)變量引起的。常量始終是線程安全的,因為只存在讀操作。局部變量是線程安全的。有狀態(tài)對象,就是有實例變量的對象,可以保存數(shù)據(jù),是非線程安全的。 前言 有多少人在使用Spring框架時,很多時候不知道或者忽視了多線程的問題? ??因為寫程序時,或做單元測試時,很難有機會碰到多線程的問題,因為沒有那么容易模擬多線程測試的環(huán)境。那么當多個線程調(diào)用同一個bean的時...
閱讀 701·2021-11-22 09:34
閱讀 3834·2021-09-22 15:42
閱讀 1346·2021-09-03 10:28
閱讀 1087·2021-08-26 14:13
閱讀 1915·2019-08-29 15:41
閱讀 1441·2019-08-29 14:12
閱讀 3379·2019-08-26 18:36
閱讀 3321·2019-08-26 13:47