ThreadLocal作用原理與內(nèi)存泄露示例解析
ThreadLocal作用
對(duì)于Android程序員來(lái)說(shuō),很多人都是在學(xué)習(xí)消息機(jī)制時(shí)候了解到ThreadLocal這個(gè)東西的。那它有什么作用呢?官方文檔大致是這么描述的:
- ThreadLocal提供了線程局部變量
- 每個(gè)線程都擁有自己的變量副本,可以通過(guò)ThreadLocal的set或者get方法去設(shè)置或者獲取當(dāng)前線程的變量,變量的初始化也是線程獨(dú)立的(需要實(shí)現(xiàn)initialValue方法)
- 一般而言ThreadLocal實(shí)例在類中被private static修飾
- 當(dāng)線程活著并且ThreadLocal實(shí)例能夠訪問到時(shí),每個(gè)線程都會(huì)持有一個(gè)到它的變量的引用
- 當(dāng)一個(gè)線程死亡后,所有ThreadLocal實(shí)例給它提供的變量都會(huì)被gc回收(除非有其它的引用指向這些變量) 上述中“變量”是指ThreadLocal的get方法獲取的值
簡(jiǎn)單例子
先來(lái)看一個(gè)簡(jiǎn)單的使用例子吧:
public class ThreadId { private static final AtomicInteger nextId = new AtomicInteger(0); private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return nextId.get(); } }; public static int get() { return threadId.get(); } }
這也是官方文檔上的例子,非常簡(jiǎn)單,就是通過(guò)在不同線程調(diào)用ThredId.get()可以獲取唯一的線程Id。如果在調(diào)用ThreadLocal的get方法之前沒有主動(dòng)調(diào)用過(guò)set方法設(shè)置值的話,就會(huì)返回initialValue方法的返回值,并把這個(gè)值存儲(chǔ)為當(dāng)前線程的變量。
ThreadLocal到底是用來(lái)解決什么問題,適用什么場(chǎng)景呢,例子是看懂了,但好像還是沒什么體會(huì)?ThreadLocal既然是提供變量的,我們不妨把我們見過(guò)的變量類型拿出來(lái),做個(gè)對(duì)比
局部變量、成員變量 、 ThreadLocal、靜態(tài)變量
變量類型 | 作用域 | 生命周期 | 線程共享性 | 作用 |
---|---|---|---|---|
局部變量 | 方法(代碼塊)內(nèi)部,其他方法(代碼塊)不能訪問 | 方法(代碼塊)開始到結(jié)束 | 只存在于每個(gè)線程的工作內(nèi)存,不能在線程中共享 | 解決變量在方法(代碼塊)內(nèi)部的代碼行之間的共享 |
成員變量 | 實(shí)例內(nèi) | 和實(shí)例相同 | 可在線程間共享 | 解決變量在實(shí)例方法之間的共享,否則方法之間只能靠參數(shù)傳遞變量 |
靜態(tài)變量 | 類內(nèi)部 | 和類的生命周期相同 | 可在多個(gè)線程間共享 | 解決變量在多個(gè)實(shí)例之間的共享 |
ThreadLocal存儲(chǔ)的變量 | 整個(gè)線程 | 一般而言與線程的生命周期相同 | 不再多線程間共享 | 解決變量在單個(gè)線程中的共享問題,線程中處處可訪問 |
ThreadLocal存儲(chǔ)的變量本質(zhì)上間接算是Thread的成員變量,ThreadLocal只是提供了一種對(duì)開發(fā)者透明的可以為每個(gè)線程存儲(chǔ)同一維度成員變量的方式。
共享 or 隔離
網(wǎng)上有很多人持有如下的看法: ThreadLocal為解決多線程程序的并發(fā)問題提供了一種新思路或者ThreadLocal是為了解決多線程訪問資源時(shí)的共享問題。 個(gè)人認(rèn)為這些都是錯(cuò)誤的,ThreadLocal保存的變量是線程隔離的,與資源共享沒有任何關(guān)系,也沒有解決什么并發(fā)問題,這一點(diǎn)看了ThreadLocal的原理就會(huì)更加清楚。就好比上面的例子,每個(gè)線程應(yīng)該有一個(gè)線程Id,這并不是什么并發(fā)問題啊。
同時(shí)他們會(huì)拿ThreadLocal與sychronized做對(duì)比,我們要清楚它們根本不是為了解決同一類問題設(shè)計(jì)的。sychronized是在牽涉到共享變量時(shí)候,要做到線程間的同步,保證并發(fā)中的原子性與內(nèi)存可見性,典型的特征是多個(gè)線程會(huì)訪問相同的變量。而ThreadLocal根本不是解決線程同步問題的,它的場(chǎng)景是A線程保存的變量只有A線程需要訪問,而其它的線程并不需要訪問,其他線程也只訪問自己保存的變量。
原理
我們來(lái)一個(gè)開放性的問題,假如現(xiàn)在要給每個(gè)線程增加一個(gè)線程Id,并且Java的Thread類你能隨便修改,你要怎么操作?非常簡(jiǎn)單吧,代碼大概是這樣
public class Thread{ private int id; public void setId(int id){ this.id=id; } }
那好,現(xiàn)在題目變了,我們現(xiàn)在還得為每個(gè)線程保存一個(gè)Looper對(duì)象,那怎么辦呢?再加一個(gè)Looper的字段不就好了,顯然這種做法肯定是不具有擴(kuò)展性的。那我們用一個(gè)容器類不就好了,很自然地就會(huì)想到Map,像下面這樣
public class Thread{ private Map<String,Object> map; public Map<String,Object> getMap(){ if(map==null) map=new HashMap<>(); return map; } }
然后我們?cè)诖a里就可以通過(guò)如下代碼來(lái)給Thread設(shè)置“成員變量”了
Thread.currentThread().getMap().put("id",id); Thread.currentThread().getMap().put("looper",looper);
然后可以在該線程執(zhí)行的任意地方,這樣訪問:
Looper looper=(Looper) Thread.currentThread().getMap().get("looper");
看上去還不錯(cuò),但是還是有些問題:
- 保存和獲取變量都要用到字符換key
- 因?yàn)閙ap中要保存各種值,因此泛型只得用Object,這樣獲取時(shí)候就需要強(qiáng)制轉(zhuǎn)換(可用泛型方法解)
- 當(dāng)該變量沒有作用時(shí)候,此時(shí)線程還沒有執(zhí)行完,需要手動(dòng)設(shè)置該變量為空,否則會(huì)造成內(nèi)存泄漏
為了不通過(guò)字符串訪問,同時(shí)省去強(qiáng)制轉(zhuǎn)換,我們封裝一個(gè)類,就叫ThreadLocal吧,偽代碼如下:
public class ThreadLocal<T> { public void set(T value) { Thread t = Thread.currentThread(); Map map = t.getMap(); if (map != null) //以自己為鍵 map.put(this, value); else createMap(t, value); } public T get() { Thread t = Thread.currentThread(); Map<ThreadLocal<?>,T> map = t.getMap(); if (map != null) { T e = map.get(this); return e; } return setInitialValue(); } }
沒錯(cuò),以上基本上就是ThreadLocal的整體設(shè)計(jì)了,只是線程中存儲(chǔ)數(shù)據(jù)的Map是特意實(shí)現(xiàn)的ThreadLocal.ThreadLocalMap。
ThredLocal本身并不存儲(chǔ)變量,只是向每個(gè)線程的threadLocals中存儲(chǔ)鍵值對(duì)。ThreadLocal橫跨線程,提供一種類似切面的概念,這種切面是作用在線程上的。
我們對(duì)ThreadLocal已經(jīng)有一個(gè)整體的認(rèn)識(shí)了,接下來(lái)我們大致看一下源碼
源碼分析
TheadLocal
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
set方法通過(guò)Thread.currentThread方法獲取當(dāng)前線程,然后調(diào)用getMap方法獲取線程的threadLocals字段,并往ThreadLocalMap中放入鍵值對(duì),其中鍵為ThreadLocal實(shí)例自己。
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
接著看get方法:
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(); }
很清晰,其中值得注意的是最后一行的setInitialValue方法,這個(gè)方法在我們沒有調(diào)用過(guò)set方法時(shí)候調(diào)用。
private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }
setInitialValue方法會(huì)獲取initialValue的返回值并把它放進(jìn)當(dāng)前線程的threadLocals中。默認(rèn)情況下initialValue返回null,我們可以實(shí)現(xiàn)這個(gè)方法來(lái)對(duì)變量進(jìn)行初始化,就像上面TheadId的例子一樣。
remove方法,從當(dāng)前線程的ThreadLocalMap中移除元素。
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
TheadLocalMap
看ThreadLocalMap的代碼我們主要是關(guān)注以下兩個(gè)方面:
- 散列表的一般設(shè)計(jì)問題。包括散列函數(shù),散列沖突問題解決,負(fù)載因子,再散列等。
- 內(nèi)存泄漏的相關(guān)處理。一般而言ThreadLocal 引用使用private static修飾,但是假設(shè)某種情況下我們真的不再需要使用它了,手動(dòng)把引用置空。上面我們知道TreadLocal本身作為鍵存儲(chǔ)在TheadLocalMap中,而ThreadLocalMap又被Thread引用,那線程沒結(jié)束的情況下ThreadLocal能被回收嗎?
散列函數(shù) 先來(lái)理一下散列函數(shù)吧,我們?cè)谥蟮拇a中會(huì)看到ThreadLocalMap通過(guò) int i = key.threadLocalHashCode & (len-1);
決定元素的位置,其中表大小len為2的冪,因此這里的&操作相當(dāng)于取模。另外我們關(guān)注的是threadLocalHashCode的取值。
private final int threadLocalHashCode = nextHashCode(); private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647;
這里很有意思,每個(gè)ThreadLocal實(shí)例的threadLocalHashCode是在之前ThreadLocal實(shí)例的threadLocalHashCode上加 0x61c88647,為什么偏偏要加這么個(gè)數(shù)呢? 這個(gè)魔數(shù)的選取與斐波那契散列有關(guān)以及黃金分割法有關(guān),具體不是很清楚。它的作用是這樣產(chǎn)生的值與2的冪取模后能在散列表中均勻分布,即便擴(kuò)容也是如此??聪旅嬉欢未a:
public class MagicHashCode { //ThreadLocal中定義的魔數(shù) private static final int HASH_INCREMENT = 0x61c88647; public static void main(String[] args) { hashCode(16);//初始化16 hashCode(32);//2倍擴(kuò)容 hashCode(64); } private static void hashCode(int length){ int hashCode = 0; for(int i=0;i<length;i++){ hashCode = i*HASH_INCREMENT+HASH_INCREMENT; System.out.print(hashCode & (length-1));//求取模后的下標(biāo) System.out.print(" "); } System.out.println(); } }
輸出結(jié)果為:
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 //容量為16時(shí)
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 //容量為32時(shí)
7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0 //容量為64時(shí)
因?yàn)門hreadLocalMap使用線性探測(cè)法解決沖突(下文會(huì)看到),均勻分布的好處在于發(fā)生了沖突也能很快找到空的slot,提高效率。
瞄一眼成員變量:
/** * 初始容量,必須是2的冪。這樣的話,方便把取模運(yùn)算轉(zhuǎn)化為與運(yùn)算, * 效率高 */ private static final int INITIAL_CAPACITY = 16; /** * 容納Entry元素,長(zhǎng)度必須是2的冪 */ private Entry[] table; /** * table中的元素個(gè)數(shù). */ private int size = 0; /** * table里的元素達(dá)到這個(gè)值就需要擴(kuò)容了 * 其實(shí)是有個(gè)裝載因子的概念的 */ private int threshold; // Default to 0
構(gòu)造函數(shù):
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); }
firstKey和firstValue就是Map存放的第一個(gè)鍵值對(duì)嘍。其中firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
很關(guān)鍵,就是當(dāng)容量為2的冪時(shí)候,這相當(dāng)于一個(gè)取模操作。然后把Entry存儲(chǔ)到數(shù)組的第i個(gè)位置,設(shè)置擴(kuò)容的閾值。
private void setThreshold(int len) { threshold = len * 2 / 3; }
這說(shuō)明當(dāng)數(shù)組里的元素容量達(dá)到2/3時(shí)候就要擴(kuò)容,也就是裝載因子是2/3。 接下來(lái)我們來(lái)看下Entry
static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
就這么點(diǎn)東西,這個(gè)Entry只是與HashMap不同,只是個(gè)普通的鍵值對(duì),沒有鏈表結(jié)構(gòu)相關(guān)的東西。
另外Entry只持有對(duì)鍵,也就是ThreadLocal的弱引用,那么我們上面的第二個(gè)問題算是有答案了。當(dāng)沒有其他強(qiáng)引用指向ThreadLocal的時(shí)候,它其實(shí)是會(huì)被回收的。
但是這有引出了另外一個(gè)問題,那Entry呢?當(dāng)鍵都為空的時(shí)候這個(gè)Entry也是沒有什么作用啊,也應(yīng)該被回收啊。不慌,我們接著往下看。
set方法:
private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); //如果沖突的話,進(jìn)入該循環(huán),向后探測(cè) for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //判斷鍵是否相等,相等的話只要更新值就好了 if (k == key) { e.value = value; return; } if (k == null) { //該Entry對(duì)應(yīng)的ThreadLocal已經(jīng)被回收,執(zhí)行replaceStaleEntry并返回 replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; //進(jìn)行啟發(fā)式清理,如果沒有清理任何元素并且表的大小超過(guò)了閾值,需要擴(kuò)容并重哈希 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
我們發(fā)現(xiàn)如果發(fā)生沖突的話,整體邏輯會(huì)一直調(diào)用nextIndex方法去探測(cè)下一個(gè)位置,直到找到?jīng)]有元素的位置,邏輯上整個(gè)表是一個(gè)環(huán)形。下面是nextIndex的代碼,就是加1而已。
private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }
線性探測(cè)的過(guò)程中,有一種情況是需要清理對(duì)應(yīng)Entry的,也就是Entry的key為null,我們上面討論過(guò)這種情況下的Entry是無(wú)意義的。
因此調(diào)用 replaceStaleEntry(key, value, i);
在看replaceStaleEntry(key, value, i)我們先明確幾個(gè)問題。
采用線性探測(cè)發(fā)解決沖突,在插入過(guò)程中產(chǎn)生沖突的元素之前一定是沒有空的slot的。這樣在也確保在查找過(guò)程,查找到空的slot就可以停止啦。
但是假如我們刪除了一個(gè)元素,就會(huì)破壞這種情況,這時(shí)需要對(duì)表中刪除的元素后面的元素進(jìn)行再散列,以便填上空隙。
空slot:即該位置沒有元素
無(wú)效slot:該位置有元素,但key為null
replaceStaleEntry除了將value放入合適的位置之外,還會(huì)在前后連個(gè)空的slot之間做一次清理expungeStaleEntry,清理掉無(wú)效slot。
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // 向前掃描到一個(gè)空的slot為止,找到離這個(gè)空slot最近的無(wú)效slot,記錄為slotToExpunge int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) { if (e.get() == null) { slotToExpunge = i; } } // 向后遍歷table for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // 找到了key,將其與無(wú)效slot交換 if (k == key) { // 更新對(duì)應(yīng)slot的value值 e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; //如果之前還沒有探測(cè)到過(guò)其他無(wú)效的slot if (slotToExpunge == staleSlot) { slotToExpunge = i; } // 從slotToExpunge開始做一次連續(xù)段的清理,再做一次啟發(fā)式清理 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // 如果當(dāng)前的slot已經(jīng)無(wú)效,并且向前掃描過(guò)程中沒有無(wú)效slot,則更新slotToExpunge為當(dāng)前位置 if (k == null && slotToExpunge == staleSlot) { slotToExpunge = i; } } // 如果key之前在table中不存在,則放在staleSlot位置 tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // 在探測(cè)過(guò)程中如果發(fā)現(xiàn)任何其他無(wú)效slot,連續(xù)段清理后做啟發(fā)式清理 if (slotToExpunge != staleSlot) { cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); } }
expungeStaleEntry主要是清除連續(xù)段之前無(wú)效的slot,然后對(duì)元素進(jìn)行再散列。返回下一個(gè)空的slot位置。
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // 刪除 staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; 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 { //對(duì)元素進(jìn)行再散列 int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
啟發(fā)式地清理: i對(duì)應(yīng)是非無(wú)效slot(slot為空或者有效) n是用于控制控制掃描次數(shù) 正常情況下如果log n次掃描沒有發(fā)現(xiàn)無(wú)效slot,函數(shù)就結(jié)束了。 但是如果發(fā)現(xiàn)了無(wú)效的slot,將n置為table的長(zhǎng)度len,做一次連續(xù)段的清理,再?gòu)南乱粋€(gè)空的slot開始繼續(xù)掃描。
這個(gè)函數(shù)有兩處地方會(huì)被調(diào)用,一處是插入的時(shí)候可能會(huì)被調(diào)用,另外個(gè)是在替換無(wú)效slot的時(shí)候可能會(huì)被調(diào)用, 區(qū)別是前者傳入的n為實(shí)際元素個(gè)數(shù),后者為table的總?cè)萘俊?/p>
private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { // i在任何情況下自己都不會(huì)是一個(gè)無(wú)效slot,所以從下一個(gè)開始判斷 i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { // 擴(kuò)大掃描控制因子 n = len; removed = true; // 清理一個(gè)連續(xù)段 i = expungeStaleEntry(i); } } while ((n >>>= 1) != 0); return removed; }
接著看set函數(shù),如果循環(huán)過(guò)程中沒有返回,找到合適的位置,插入元素,表的size增加1。這個(gè)時(shí)候會(huì)做一次啟發(fā)式清理,如果啟發(fā)式清理沒有清理掉任何無(wú)效元素,判斷清理前表的大小大于閾值threshold的話,正常就要進(jìn)行擴(kuò)容了,但是表中可能存在無(wú)效元素,先把它們清除掉,然后再判斷。
private void rehash() { // 全量清理 expungeStaleEntries(); //因?yàn)樽隽艘淮吻謇?,所以size可能會(huì)變小,這里的實(shí)現(xiàn)是調(diào)低閾值來(lái)判斷是否需要擴(kuò)容。 threshold默認(rèn)為len*2/3,所以這里的threshold - threshold / 4相當(dāng)于len/2。 if (size >= threshold - threshold / 4) { resize(); } }
作用即清除所有無(wú)效slot
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); } } }
保證table的容量len為2的冪,擴(kuò)容時(shí)候要擴(kuò)大2倍
private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; } else { // 擴(kuò)容后要重新放置元素 int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) { h = nextIndex(h, newLen); } newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }
get方法:
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; // 對(duì)應(yīng)的entry存在且key未被回收 if (e != null && e.get() == key) { return e; } else { // 繼續(xù)往后查找 return getEntryAfterMiss(key, i, e); } } private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; // 不斷向后探測(cè)直到遇到空entry while (e != null) { ThreadLocal<?> k = e.get(); // 找到 if (k == key) { return e; } if (k == null) { // 該entry對(duì)應(yīng)的ThreadLocal實(shí)例已經(jīng)被回收,調(diào)用expungeStaleEntry來(lái)清理無(wú)效的entry expungeStaleEntry(i); } else { // 下一個(gè)位置 i = nextIndex(i, len); } e = tab[i]; } return null; }
remove方法,比較簡(jiǎn)單,在table中找key,如果找到了斷開弱引用,做一次連續(xù)段清理。
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(); // 連續(xù)段清理 expungeStaleEntry(i); return; } } }
ThreadLocal與內(nèi)存泄漏
從上文我們知道當(dāng)調(diào)用ThreadLocalMap的set或者getEntry方法時(shí)候,有很大概率會(huì)去自動(dòng)清除掉key為null的Entry,這樣就可以斷開value的強(qiáng)引用,使對(duì)象被回收。
但是如果如果我們之后再也沒有在該線程操作過(guò)任何ThreadLocal實(shí)例的set或者get方法,那么就只能等線程死亡才能回收無(wú)效value。
因此當(dāng)我們不需要用ThreadLocal的變量時(shí)候,顯示調(diào)用ThreadLocal的remove方法是一種好的習(xí)慣。
小結(jié)
- ThredLocal為每個(gè)線程保存一個(gè)自己的變量,但其實(shí)ThreadLocal本身并不存儲(chǔ)變量,變量存儲(chǔ)在線程自己的實(shí)例變量
ThreadLocal.ThreadLocalMap threadLocals
中 - ThreadLocal的設(shè)計(jì)并不是為了解決并發(fā)問題,而是解決一個(gè)變量在線程內(nèi)部的共享問題,在線程內(nèi)部處處可以訪問
- 因?yàn)槊總€(gè)線程都只會(huì)訪問自己ThreadLocalMap 保存的變量,所以不存在線程安全問題
以上就是ThreadLocal作用原理與內(nèi)存泄露示例解析的詳細(xì)內(nèi)容,更多關(guān)于ThreadLocal內(nèi)存泄露的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot實(shí)現(xiàn)配置兩個(gè)parent的方法
這篇文章主要介紹了springboot實(shí)現(xiàn)配置兩個(gè)parent的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12Java實(shí)現(xiàn)對(duì)一行英文進(jìn)行單詞提取功能示例
這篇文章主要介紹了Java實(shí)現(xiàn)對(duì)一行英文進(jìn)行單詞提取功能,結(jié)合實(shí)例形式分析了java基于StringTokenizer類進(jìn)行字符串分割的相關(guān)操作技巧,需要的朋友可以參考下2017-10-10Java之Swagger配置掃描接口以及開關(guān)案例講解
這篇文章主要介紹了Java之Swagger配置掃描接口以及開關(guān)案例講解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08java Arrays快速打印數(shù)組的數(shù)據(jù)元素列表案例
這篇文章主要介紹了java Arrays快速打印數(shù)組的數(shù)據(jù)元素列表案例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09使用Java實(shí)現(xiàn)qq郵箱發(fā)送郵件
這篇文章主要為大家詳細(xì)介紹了使用Java實(shí)現(xiàn)qq郵箱發(fā)送郵件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2010-05-05Springboot?整合?RocketMQ?收發(fā)消息的配置過(guò)程
這篇文章主要介紹了Springboot?整合?RocketMQ?收發(fā)消息,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-12-12