摘要:底層是一個的散列表可擴容的數組,并采用開放地址法來解決沖突。稍后討論方法每個對象都有一個值,每初始化一個對象,值就增加一個固定的大小。因此在使用的時候要手動調用方法,防止內存泄漏。
ThreadLocal定義
先看JDK關于ThreadLocal的類注釋:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
翻譯過來大概的意思為:ThreadLocal提供了線程內部的局部變量;每個線程都有自己的,獨立的初始化變量副本;ThreadLocal實例通常是類中的private static字段,該類一般在線程狀態相關(或線程上下文)中使用。
Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).
翻譯過來大概的意思為:只要線程處于活動狀態且ThreadLocal實例是可訪問的狀態下,每個線程都持有對其線程局部變量副本的隱式引用;在線程消亡后,ThreadLocal實例s的所有副本都將進行垃圾回收(除非存在對這些副本的其他引用)。
一些應用場景1、多線程下使用日志追蹤,如Logback或Log4j的MDC組件
2、在事務中,connection綁定到當前線程來保證這個線程中的數據庫操作用的是同一個connection
3、dubbo的RpcContext的實現:
4、現在的分布式trace系統中的traceId、spanId的傳遞等
5、web前臺的請求參數,在同一線程內多個方法之間隱式傳遞
。。。
一個簡單的demo:
public class TraceContext { private static final ThreadLocaltraceIdHolder = new ThreadLocal () { @Override protected String initialValue() {//① return UUID.randomUUID().toString().replaceAll("-", ""); } }; public static void setTraceId(String traceId) { traceIdHolder.set(traceId);//② } public static String getTraceId() { return traceIdHolder.get();//③ } public static void removeTraceId() { traceIdHolder.remove();//④ } }
思考:ThreadLocal類型的traceIdHolder一般被修飾為static、final、private,就是traceIdHolder在被使用的時候為單例不可變(這不是常見的單例飽漢模式么)。如果traceIdHolder定義為多實例會怎么樣?
ThreadLocal實現解讀以下以JDK1.8實現解讀
ThreadLocal的構造函數為空:public ThreadLocal() {}
ThreadLocal的set方法:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
獲取當前線程的ThreadLocalMap,有直接設置value、沒有新建
ThreadLocal的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(); }
從當前線程的ThreadLocalMap中查找Entry,如果不必為null返回value,否則設置初值并返回setInitialValue()
ThreadLocal的remove()方法:
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
從當前線程的ThreadLocalMap中刪除
ThreadLocal的setInitialValue()方法:
private T setInitialValue() { T value = initialValue();//未覆蓋就是null Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }
和set類似
查看Thread的threadLocals的字段定義:ThreadLocal.ThreadLocalMap threadLocals = null;
查看ThreadLocal的內部類ThreadLocalMap的定義:
雖然ThreadLocalMap命名含有"Map",但和Map接口沒任何關系。ThreadLocalMap底層是一個的散列表(可擴容的數組),并采用開放地址法來解決hash沖突。
ThreadLocalMap.Entry定義:
static class Entry extends WeakReference> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal> k, Object v) { super(k); value = v; } }
這里先不管為啥使用WeakReference定義。稍后討論
ThreadLocalMap.set方法:
private void set(ThreadLocal> key, Object value) { // We don"t use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. 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)]) { ThreadLocal> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
每個ThreadLocal對象都有一個hash值threadLocalHashCode,每初始化一個ThreadLocal對象,hash值就增加一個固定的大小0x61c88647。
定義如下:
private final int threadLocalHashCode = nextHashCode(); /** * The next hash code to be given out. Updated atomically. Starts at * zero. */ private static AtomicInteger nextHashCode = new AtomicInteger(); /** * The difference between successively generated hash codes - turns * implicit sequential thread-local IDs into near-optimally spread * multiplicative hash values for power-of-two-sized tables. */ private static final int HASH_INCREMENT = 0x61c88647; /** * Returns the next hash code. */ private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }
ThreadLocalMap.set流程總結如下:
根據當前ThreadLocal的hashCode mod table.length,計算出應插入的table位置下表i;
如果table[i]的Entry不為null,
①、判斷Entry.key == 當前的ThreadLocal對象?相等覆蓋舊值 退出
②、如果Entry.key為null,將執行刪除兩個null槽之間的所有的過期的stale的entry,并把當前的位置i上初始化一個Entry對象,退出
③、繼續查找下一個位置i++
如果找到了一個位置k,table[k]為null,初始化一個Entry對象
ThreadLocalMap.getEntry方法:
private Entry getEntry(ThreadLocal> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); } private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal> k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; } private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null 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 { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
ThreadLocalMap.getEntry流程總結如下:
根據當前ThreadLocal的hashCode mod table.length,計算直接索引的位置i,如果e不為null并且key相同則返回e。
如果e為null,返回null
如果e不為空且key不相同,則查找下一個位置,繼續查找比較,直到e為null退出
在查找的過程中如果發現e不為空,且e的k為空的話,刪除當前槽和下一個null槽之間的所有過期entry對象
ThreadLocalMap.remove方法:
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(); expungeStaleEntry(i); return; } } }
ThreadLocalMap.remove流程總結如下:
計算直接索引的位置i,如果table[i]的entry e不為null,且key比較相等,則執行刪除,把table[i]=null,table[i].value = null;然后刪除當前槽和下一個null槽之間的所有過期entry對象
查找下一個位置,i++,直到table[i]的entry e為null退出
總結ThreadLocalMap:
散列采用開放地址,線性探測,在hash沖突較大的時候效率低下
ThreadLocalMap的set、get、remove操作中都帶有刪除過期元素的操作,類似緩存的lazy淘汰
Thread、ThreadLoal、ThreadLocalMap關系圖:
整體的對象關系圖
關于ThreadLocal的內存泄漏以下分析轉自知乎作者winwill2012,鏈接:我覺得是這樣的
如上圖,ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用引用他,那么系統gc的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當前線程再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鏈:
ThreadRef -> Thread -> ThreaLocalMap -> Entry -> value
永遠無法回收,造成內存泄露。
因此在使用ThreadLocal的時候要手動調用remove方法,防止內存泄漏。
JDK建議將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命周期就更長(類的靜態屬性引用的對象為GCRoots),由于一直存在ThreadLocal的強引用,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱引用訪問到Entry的value值,然后remove它,防止內存泄露。
我覺的JDK建議將ThreadLocal變量定義成private static的還有個可能原因是:單例,ThreadLocal對象是無狀態的,無含義的,聲明同一類型的ThreadLocal對象多實例,浪費ThreadLocalMap的存儲空間且對象更容易引起內存泄漏。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/71095.html
摘要:方法,刪除當前線程綁定的這個副本數字,這個值是的值,普通的是使用鏈表來處理沖突的,但是是使用線性探測法來處理沖突的,就是每次增加的步長,根據參考資料所說,選擇這個數字是為了讓沖突概率最小。 showImg(https://segmentfault.com/img/remote/1460000019828633); 老套路,先列舉下關于ThreadLocal常見的疑問,希望可以通過這篇學...
摘要:雖然類名中帶有字樣,但是實際上并不是接口的子類。是弱連接接口,這意味著如果僅有指向某一類,其任然有可能被回收掉。這里使用弱連接的意義,是為了防止業務代碼中置空對象,但是由于存在連接可達,所以仍然無法回收掉該對象的情況發生。 零 前期準備 0 FBI WARNING 文章異常啰嗦且繞彎。 1 版本 JDK 版本 : OpenJDK 11.0.1 IDE : idea 2018.3 2 T...
摘要:常見標高線程上下文切換頻繁線程太多鎖競爭激烈標高如果的占用很高,排查涉及到的程序,比如把改造成。抖動問題原因字節碼轉為機器碼需要占用時間片,大量的在執行字節碼時,導致長期處于高位現象,占用率最高解決辦法保證編譯線程的占比。 一、并發 Unable to create new native thread …… 問題1:Java中創建一個線程消耗多少內存? 每個線程有獨自的棧內存,共享堆內...
摘要:通過向消息池發送各種消息事件通過處理相應的消息事件。子線程往消息隊列發送消息,并且往管道文件寫數據,主線程即被喚醒,從管道文件讀取數據,主線程被喚醒只是為了讀取消息,當消息讀取完畢,再次睡眠。 目錄介紹 6.0.0.1 談談消息機制Hander作用?有哪些要素?流程是怎樣的? 6.0.0.2 為什么一個線程只有一個Looper、只有一個MessageQueue,可以有多個Handle...
閱讀 3478·2023-04-26 02:00
閱讀 3078·2021-11-22 13:54
閱讀 1699·2021-08-03 14:03
閱讀 709·2019-08-30 15:52
閱讀 3085·2019-08-29 12:30
閱讀 2420·2019-08-26 13:35
閱讀 3364·2019-08-26 13:25
閱讀 3001·2019-08-26 11:39