.net 建網(wǎng)站線上教育培訓(xùn)機構(gòu)十大排名
問題的提出
- 在Java多線程中,共享變量的讀寫非常容易出現(xiàn)不可預(yù)測的行為,因此對共享變量的訪問控制非常重要。因此在多線程編程時,為了保證線程安全,需要進行額外的同步措施。比如典型的操作就是加鎖。除了加鎖外,另一種常用的方法是使用ThreadLocal , 這種用法在框架中非常普遍。
- ThreadLocal可以理解為線程本地變量。多個線程對同一個變量如何不互相影響,那么只有對自身獨有的變量訪問(獨占內(nèi)存)時才是線程安全的,因此,ThreadLocal 為變量在每個線程中都創(chuàng)建了一個副本,該副本只能被當(dāng)前線程訪問,多線程之間是隔離的,變量不能在多線程之間共享。這樣每個線程修改變量副本時,不會對其他線程產(chǎn)生影響。
常規(guī)做法
- 多線程訪問 ThreadLocal 變量時都會有自己獨立的實例副本,要維護線程與變量的對應(yīng)關(guān)系,一種普遍的思維就是在 ThreadLocal 中維護一個映射表 Map 用于記錄線程與實例之間的映射關(guān)系,也就是有一層總控的協(xié)調(diào)在里面。
- 但是,如果存在總控進行協(xié)調(diào),那么在新增線程和銷毀線程時都需要更新 Map 中的映射關(guān)系時,就會在總控這里產(chǎn)生多線程并發(fā)修改,那么就又產(chǎn)生了額外的操作來保證 Map 是線程安全的。如果是在高并發(fā)的場景并發(fā)修改 Map 需要加鎖,會明顯降低性能。
JDK的實現(xiàn)
-
JDK的 ThreadLocal 提供了一種新的思路,從 Thread 的視角來看,在 Thread 中維護一個 Map用于記錄 ThreadLocal 與實例之間的映射關(guān)系,這樣在同一個線程內(nèi),Map 就不需要加鎖了。
-
從 Thread 代碼定義看出:Thread類依賴了ThreadLocal.ThreadLocalMap;
public class Thread implements Runnable {/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null;
}
- 從 ThreadLocal 的代碼來看:
- 它的get/set都是從線程取到對應(yīng)的ThreadLocal.ThreadLocalMap;代碼僅以get示例。
- 它定義的ThreadLocalMap的 Entry 繼承了WeakReference,Entry的key是弱引用,value是強引用。這樣設(shè)計獲得了另外一個好處:防止內(nèi)存泄漏。在 JVM 垃圾回收時,只要發(fā)現(xiàn)了弱引用的對象,不管內(nèi)存是否充足,都會被回收。如果key是強引用,當(dāng)ThreadLocal不再使用時,ThreadLocalMap中還是存在對ThreadLocal的強引用,那么GC是無法回收的,從而造成內(nèi)存泄漏。
public class ThreadLocal<T> {/*** Returns the value in the current thread's copy of this* thread-local variable. If the variable has no value for the* current thread, it is first initialized to the value returned* by an invocation of the {@link #initialValue} method.** @return the current thread's value of this thread-local*/public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}static class ThreadLocalMap {/*** The entries in this hash map extend WeakReference, using* its main ref field as the key (which is always a* ThreadLocal object). Note that null keys (i.e. entry.get()* == null) mean that the key is no longer referenced, so the* entry can be expunged from table. Such entries are referred to* as "stale entries" in the code that follows.*/static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}}
}
- Entry的key設(shè)計成了弱引用,但是當(dāng)ThreadLocal不再使用被 GC 回收后,ThreadLocalMap 中可能出現(xiàn) Entry的key為
null
,那么Entry的value一直會強引用數(shù)據(jù)而得不到釋放,只能等待線程銷毀。 - 如何避免ThreadLocalMap內(nèi)存泄漏? ThreadLocal做了如下的保護措施,在執(zhí)行 ThreadLocal.set()/get()方法時,ThreadLocal會調(diào)用expungeStaleEntry方法清除 ThreadLocalMap 中key為
null
的 Entry 對象,讓它還能夠被 GC 回收。- expungeStaleEntry方法會遍歷ThreadLocalMap的散列表,從當(dāng)前節(jié)點開始向后遍歷數(shù)組,將過期的條目(即key為null的條目)清理掉,并將這些條目的位置設(shè)置為null。如果遇到未過期的數(shù)據(jù),則重新計算其位置并重新分配,確保數(shù)據(jù)盡可能靠近正確的位置,以減少沖突和查找時間?。
編程習(xí)慣
在編程時,還需要注意:
- 當(dāng)線程中某個ThreadLocal對象不再使用時,立即調(diào)用
remove()
方法刪除Entry對象。 - 如果是在異常的場景中,應(yīng)在
finally
代碼塊中進行清理,保持良好的異常處理意識。
學(xué)得經(jīng)驗
- 除了集中控制并發(fā)外,可以將競爭的數(shù)據(jù)分散出去。
- 解決方案的自洽性,增加了線程自身的數(shù)據(jù)副本,需要保證生命周期的一致性,這里的設(shè)計盡最大可能的保證了GC的可執(zhí)行。