Java并發-ThreadLocal

原文地址:cmsblogs.com/?p=2442

ThreadLocal介紹

ThreadLocal提供了一種解決多線程環境下成員變量的問題,但是它并不是解決多線程共享變量的問題。那么ThreadLocal到底是什么呢?

API是這樣介紹的: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).

該類提供了線程局部(thread-local)變量。這些變量不同于普通對應物,因為訪問某個變量(通過其get或set方法)的每個線程都有自己的局部變量,它獨立于變量的初始化副本。ThreadLocal實例通常是類中的private static字段,它們希望將狀態與某一個線程(例如,用戶ID或事務ID)相關聯。

ThreadLocal與線程同步機制不同,線程同步機制是多個線程共享同一個變量,而ThreadLocal為了每一個線程創建一個單獨的變量副本,故而每個線程都可以獨立地改變自己所擁有的變量副本,而不會影響其他線程所對應的副本。

ThreadLocal使用示例,代碼如下:

public class SeqCount {private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>() {// 實現initialValue()public Integer initialValue() {return 0;}};public int nextSeq() {seqCount.set(seqCount.get() + 1);return seqCount.get();}public static void main(String[] args) {SeqCount seqCount = new SeqCount();SeqThread thread1 = new SeqThread(seqCount);SeqThread thread2 = new SeqThread(seqCount);SeqThread thread3 = new SeqThread(seqCount);SeqThread thread4 = new SeqThread(seqCount);thread1.start();thread2.start();thread3.start();thread4.start();}private static class SeqThread extends Thread {private SeqCount seqCount;SeqThread(SeqCount seqCount) {this.seqCount = seqCount;}public void run() {for (int i = 0; i < 3; i++) {System.out.println(Thread.currentThread().getName() + " seqCount :"+ seqCount.nextSeq());}}}
}
復制代碼

ThreadLocal實現原理

ThreadLocal的實現是這樣的:每個Thread維護一個ThreadLocalMap映射表,這個映射表的keyThreadLocal實例本身,value是真正需要存儲的Object

也就是說ThreadLocal本身并不存儲值,它只是作為一個key來讓線程從ThreadLocalMap獲取 value。值得注意的是圖中的虛線,表示ThreadLocalMap是使用ThreadLocal的弱引用作為Key的,弱引用的對象在GC時會被回收。

ThreadLocal源碼分析

ThreadLocalMap

ThreadLocalMap的構造函數如下:

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);
}
復制代碼

由上可知,ThreadLocalMap其內部利用Entry來實現key-value的存儲,如下:

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就是ThreadLocal,而value就是值。同時,Entry也繼承WeakReference,所以說Entry所對應key(ThreadLocal實例)的引用為一個弱引用

接下來,看看ThreadLocalMap最核心的方法set(ThreadLocal> key, Object value)、getEntry()方法。

1、set(ThreadLocal<?> key, Object value)

private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;// 根據 ThreadLocal 的散列值,查找對應元素在數組中的位置int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();// key 存在,直接覆蓋if (k == key) {e.value = value;return;}// key == null,但是存在值(因為此處的e != null),說明之前的ThreadLocal對象已經被回收if (k == null) {// 用新元素替換陳舊的元素replaceStaleEntry(key, value, i);return;}}// ThreadLocal對應的key實例不存在則創建tab[i] = new Entry(key, value);int sz = ++size;// cleanSomeSlots 清楚陳舊的Entry(key == null)// 如果沒有清理陳舊的 Entry 并且數組中的元素大于了閾值,則進行 rehashif (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}復制代碼

set()操作除了存儲元素外,還有一個很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),這兩個方法可以清除掉key == null 的實例,防止內存泄漏。在set()方法中還有一個變量很重要:threadLocalHashCode,定義如下:

private final int threadLocalHashCode = nextHashCode();
復制代碼

從名字上面我們可以看出threadLocalHashCode應該是ThreadLocal的散列值,定義為final,表示ThreadLocal一旦創建其散列值就已經確定了,生成過程則是調用nextHashCode():

private static AtomicInteger nextHashCode = new AtomicInteger();private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
}
復制代碼

nextHashCode表示分配下一個ThreadLocal實例的threadLocalHashCode的值,HASH_INCREMENT則表示分配兩個ThradLocal實例的threadLocalHashCode的增量。

2、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;elsereturn getEntryAfterMiss(key, i, e);
}
復制代碼

采用了開放定址法,所以當前key的散列值和元素在數組的索引并不是完全對應的,首先取一個探測數(key的散列值),如果所對應的key就是我們所要找的元素,則返回,否則調用getEntryAfterMiss()

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;// 當key == null時,調用了expungeStaleEntry()方法,該方法用于處理key == null,// 有利于GC回收,能夠有效地避免內存泄漏。if (k == null)expungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}return null;}
復制代碼

ThreadLocal核心方法

set(T value):設置當前線程的線程局部變量的值

public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}
復制代碼

獲取當前線程所對應的ThreadLocalMap,如果不為空,則調用ThreadLocalMap的set()方法,key就是當前ThreadLocal,如果不存在,則調用createMap()方法創建。

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;}}// 如果ThreadLocalMap不存在,返回初始值。return setInitialValue();
}
復制代碼

首先通過當前線程獲取所對應的成員變量ThreadLocalMap,然后通過ThreadLocalMap獲取當前ThreadLocalEntry,最后通過所獲取的Entry獲取目標值result。

initialValue():返回該線程局部變量的初始值

protected T initialValue() {return null;
}
復制代碼

這個方法將在一個線程第一次使用get方法訪問變量時被調用,除非線程先前調用了set方法,在這種情況下,線程不會調用initialValue方法。通常情況下,每個線程最多調用一次此方法,但在后續調用removeget時,可能會再次調用此方法。

默認實現返回null,如果程序員希望線程局部變量具有非null的初始值,則必須對ThreadLocal進行子類化,并重寫此方法。

remove():將當前線程局部變量的值刪除

public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);
}
復制代碼

該方法的目的是減少內存的占用。當然,我們不需要顯示調用該方法,因為一個線程結束后,它所對應的局部變量就會被垃圾回收。

ThreadLocal為什么會內存泄漏

ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用來引用它,那么系統GC的時候,這個ThreadLocal勢必會被回收,ThreadLocalMap中就會出現keynullEntry,就沒有辦法訪問這些keynullEntryvalue。如果當前線程再遲遲不結束的話,這些key為nullEntryvalue就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠無法回收,造成內存泄漏。

其實,ThreadLocalMap的設計中已經考慮到這種情況,也加上了一些防護措施:在ThreadLocalget(),set(),remove()的時候都會清除線程ThreadLocalMap里所有keynullvalue

但是這些被動的預防措施并不能保證不會內存泄漏:

  • 使用staticThreadLocal,延長了ThreadLocal的生命周期,可能導致的內存泄漏。

  • 分配使用了ThreadLocal又不再調用get(),set(),remove()方法,那么就會導致內存泄漏。

ThreadLocal內存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動刪除對應key就會導致內存泄漏,而不是因為弱引用。

理解了ThreadLocal內存泄漏的前因后果,那么怎么避免內存泄漏呢?

  • 每次使用完ThreadLocal,都調用它的remove()方法,清除數據。

在使用線程池的情況下,沒有及時清理ThreadLocal,不僅是內存泄漏的問題,更嚴重的是可能導致業務邏輯出現問題。所以,使用ThreadLocal就跟加鎖完要解鎖一樣,用完就清理。

參考資料

  1. 【死磕Java并發】—–深入分析ThreadLocal

  2. 深入分析 ThreadLocal 內存泄漏問題


如果讀完覺得有收獲的話,歡迎點贊、關注、加公眾號【牛覓技術】,查閱更多精彩歷史!!!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/538632.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/538632.shtml
英文地址,請注明出處:http://en.pswp.cn/news/538632.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

vue 監聽map數組變化_解決vue無法偵聽數組及對象屬性的變化問題

一、數組1、可以監聽到的情況如push、splice、賦值(array[1,2,3])2、無法監聽到的情況使用下標修改某個元素(這種比較常見)array[index] 1object.a 3直接修改數組lengtharray.length 53、解決方案this.$set(array, index, data) - 這是個深度的修改&#xff0c;某些情況下可…

哈維瑪德學院 計算機,這些美國大學名氣不高,卻有最頂級的工程專業

上一期我們盤點了有博士學位的Top50 大學工程專業排名今天我們繼續盤點 2018USNEWS 工程專業排名不過這個排名主要針對的是沒有博士學位的 Top50 大學和上一期那些大名鼎鼎的大學相比這里面很多大學并不怎么為人熟知因為這個榜單大部分都是區域性大學事實上&#xff0c;工程專業…

Hbase介紹

1、為什么出現hbase&#xff1f;hadoop 的NameNode適合大文件&#xff0c;不適合小文件。HDFS不適合大量小文件的存儲&#xff0c;因namenode將文件系統的元數據存放在內存中&#xff0c;因此存儲的文件數目受限于 namenode的內存大小。HDFS中每個文件、目錄、數據塊占用150Byt…

math 向上取整_自我說明:關于Math和File類的具體說明.

Math類:Math類&#xff0c;不允許有子類&#xff0c;它直接繼承于object.Math類包含執行基本數字運算的方法.如基本指數&#xff0c;對數&#xff0c;平方根和三角函數.Math的基本方法&#xff1a;System.out.pintln(“1.絕對值&#xff1a;”Math.abs(16)”t”Math.abs(-16)”…

眼圖 非差分線_LVDS低電壓差分信號簡介

LVDS低電壓差分信號簡介1. 名詞解釋1.1. 背景隨著數據傳輸速率越來越高&#xff0c;現在計算機系統中的數據傳輸接口基本上都串行化了&#xff0c;像USB、PCIe、SATA、DP等等外部總線將并行總線擠壓到只剩下內存總線這個最后的堡壘。當然&#xff0c;就算是并行傳輸總線最后的倔…

無內存在優盤可以啟動計算機嗎,沒有U盤不要緊,內存卡做啟動盤裝Win7方法

現在最流行的win7系統安裝方法大多以U盤來安裝&#xff0c;那么如果你沒有U盤呢&#xff1f;為了安裝個Win7系統是不是要去買個U盤&#xff1f;當然不用&#xff0c;如果你還有空閑不用的手機內存卡&#xff0c;那么也是可以制作U盤啟動盤來安裝Win7系統的。準備工具&#xff1…

微信小程序request請求動態獲取數據

微信小程序開發文檔鏈接 1 后臺代碼: clickButton:function(){var that this;wx.request({url: http://localhost:9096/admin.php/index/jj,method : POST,header: { content-type: application/x-www-form-urlencoded },data : {},success: function (res) {//console.log(re…

HBase中的HMaster、HRegionServer、Zookeeper

原文鏈接&#xff1a;http://blog.csdn.net/mm_bit/article/details/51304233 ----------------------------------- HMaster是Hbase主/從集群架構中的中央節點。通常一個HBase集群存在多個HMaster節點,其中一個為Active Master,其余為Backup Master. Hbase每時每刻只有一個hm…

spyder python調試_使用spyder編譯器單步調試python

1、將需要進行單步調試的函數在腳本中進行調用&#xff08;十分重要的一步&#xff09;。由于python是解釋型語言&#xff0c;在進行單步調試的時候需要告訴系統你使用了這個函數&#xff0c;單步調試才會進入你所需要調式的函數。如下圖所示&#xff0c;我們定義了createDataS…

label qt 自動換行_QT編寫一個登錄界面

前言繼上篇&#xff1a;一起學Qt之基礎篇---入門今天上手實操用QT編寫一個登錄界面~系統權限這個詞大家肯定不陌生&#xff0c;你進入一個網站也是&#xff0c;如果不登錄&#xff0c;就是以游客的身份進去的&#xff0c;要想看到某些信息肯定需要進行登錄&#xff0c;更完善的…

我的世界服務器怎么修改書與筆,我的世界書與筆怎么做 我的世界書與筆怎么用...

第一步先收集甘蔗&#xff0c;然后合成紙&#xff0c;甘蔗在河邊&#xff0c;池塘邊&#xff0c;沼澤地一般都會生成&#xff0c;我們也可以拿回家種植&#xff0c;但是甘蔗必須種在水邊&#xff0c;其他地方種植不了&#xff0c;其他地方玩家怎么右鍵甘蔗都是沒有反應的。第二…

python csv模塊用法_python使用csv模塊如何將數據存放在一張表的不同行?

def save2csv(file_nameNone, headerNone, dataNone): """ 保存成CSV格式文件,方便Excel直接打開 :param file_name: 保存的文件名 :param header: 表頭,每一列的名字 :param data: 具體填充數據 :return: """ if file_name is None or isinstan…

虛擬機 服務器 root,虛擬機切換到root賬戶

虛擬機切換到root賬戶 內容精選換一換一、安裝虛擬機1、下載VMware workstation 14 &#xff0c;安裝時按照默認配置安裝2、下載ubuntu-18.04.1-desktop-amd64.iso鏡像文件3、打開 VMware workstation &#xff0c;新建虛擬機4、配置虛擬機內存&#xff0c;磁盤&#xff0c;網絡…

每一行末尾添加分號

文本內容如下&#xff1a; TMP_TBX_100_0_A1 TMP_TBX_100_0_A10 TMP_TBX_100_0_A12 TMP_TBX_100_0_A13 TMP_TBX_100_0_A14 TMP_TBX_100_0_A15 TMP_TBX_100_0_A15_2 TMP_TBX_100_0_A16 TMP_TBX_100_0_A17 TMP_TBX_100_0_A18 TMP_TBX_100_0_A19 TMP_TBX_100_0_A19_2 TMP_TBX_100…

壯觀霉素抗性基因原理_基因組學深入挖掘·研究方案(下篇)

前情回顧上次小編為大家講解了四種以基因組為基礎的多組學聯合研究方案&#xff08;基因組與轉錄組&#xff0c;深入挖掘基因表達信息&#xff1b;基因組聯合代謝組與轉錄組&#xff0c;鎖定關鍵通路&#xff1b;基因組與群體進化&#xff0c;解析物種發展歷程&#xff1b;基因…

Shell腳本大量示例

幾乎所有的腳本里都有某種流控制結構&#xff0c;很少有例外。流控制是什么&#xff1f;假定有一個腳本&#xff0c;包含下列幾個命令&#xff1a; #!/bin/sh # make a directory mkdir /home/dave/mydocs # copy all doc files cp *.docs /home/dave/docs # delete all doc fi…

Spark 常見問題小結

原文地址&#xff1a;http://www.aboutyun.com/thread-9946-1-1.html -------------------------------------- 問題導讀 1、當前集群的可用資源不能滿足應用程序的需求&#xff0c;怎么解決&#xff1f; 2、內存里堆的東西太多了&#xff0c;有什么好辦法嗎&#xff1f; …

dataframe數據標準化處理_數據預處理——標準化/歸一化(實例)

這次我們來說說關于數據預處理中的數據標準化及歸一化的問題。主要以理論實例的方式為大家展示。本次實驗也將會展示部分數據以及代碼&#xff0c;有興趣的小伙伴可以自己動手試試~在本次實例過程中&#xff0c;我們使用的數據是&#xff1a;2010-2018年間廣州市經濟與環境的時…

python實現自動打電話軟件_全自動手勢聯系軟件 讓你輕輕松松打電話

電話號碼超級多的童鞋們&#xff0c;由于手機里存儲的電話太多&#xff0c;每次要找某個人的電話很難找&#xff0c;有木有同感的&#xff1f;小編今天像大家推薦一款新潮的幫助你輕松找到想要的電話的模式&#xff0c;有沒有很想知道呢&#xff1f;其實小編本來是想賣個關子的…

電腦的虛擬服務器位置,如何配置基于IP地址的虛擬主機

滿意答案虛擬主機簡介&#xff1a;1&#xff0e; 把一臺運行在互聯網上的服務器分成多個虛擬的服務器。2&#xff0e; 每一個虛擬主機都具有獨立的域名和完整的Internet服務器(支持WWW&#xff0c;FTP&#xff0c;E-mail等)。3&#xff0e; 一臺服務器上的不同虛擬主機是各自獨…