如何掌握ThreadLocal的相關(guān)知識點(diǎn)
本篇內(nèi)容介紹了“如何掌握ThreadLocal的相關(guān)知識點(diǎn)”的有關(guān)知識,在實(shí)際案例的操作過程中,不少人都會(huì)遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!
成都創(chuàng)新互聯(lián)始終堅(jiān)持【策劃先行,效果至上】的經(jīng)營理念,通過多達(dá)十載累計(jì)超上千家客戶的網(wǎng)站建設(shè)總結(jié)了一套系統(tǒng)有效的全網(wǎng)推廣解決方案,現(xiàn)已廣泛運(yùn)用于各行各業(yè)的客戶,其中包括:會(huì)所設(shè)計(jì)等企業(yè),備受客戶贊美。
一、介紹
根據(jù) Java 官方文檔的描述,我們可知 ThreadLocal類用于提供線程內(nèi)部的局部變量,其在多線程環(huán)境下能保證各個(gè)線程內(nèi)部變量的隔離性。
換言之,ThreadLocal提供線程內(nèi)的局部變量,不同線程之間不會(huì)相互干擾,該變量作用范圍貫穿線程的生命周期,減少同一線程內(nèi)多個(gè)方法或組件之間一些公共變量傳遞的復(fù)雜度。
二、使用
2.1 常用方法
返回值 | 方法名 | 描述 |
---|---|---|
T | get() | 返回此線程局部變量的當(dāng)前線程副本中的值 |
void | remove() | 移除此線程局部變量當(dāng)前線程的值 |
void | set(T value) | 將此線程局部變量的當(dāng)前線程副本中的值設(shè)置為指定值 |
2.2 案例演示
需求:用 3 名畫家在一個(gè)畫布上各自繪制一種顏色,并打印出其繪制的顏色。
/** * 畫布類 */ public class Canvas { private String content; public String getContent() { return content; } public void setContent(String content) { this.content = content; } } /** * 畫家類 */ public class Painter extends Thread { private String name; private Canvas canvas; private String color; public Painter(String name, Canvas canvas, String color) { this.name = name; this.canvas = canvas; this.color = color; } @Override public void run() { canvas.setContent(color); System.out.println(this.name + "在畫板繪制" + canvas.getContent()); } } /** * 啟動(dòng)類 */ public class Demo { public static void main(String[] args) { // 創(chuàng)建畫布 Canvas canvas = new Canvas(); Painter painter1 = new Painter("小強(qiáng)", canvas, "紅色"); Painter painter2 = new Painter("旺財(cái)", canvas, "黃色"); Painter painter3 = new Painter("狗蛋", canvas, "藍(lán)色"); painter1.start(); painter2.start(); painter3.start(); } }
執(zhí)行結(jié)果如下:
小強(qiáng)在畫板繪制藍(lán)色 旺財(cái)在畫板繪制黃色 狗蛋在畫板繪制黃色
顯然,在多線程訪問同一個(gè)資源(畫布)的情況下,輸出結(jié)果出現(xiàn)并發(fā)問題。
現(xiàn)有 2 種解決方案:一種是在 run方法中加入 synchronized同步代碼塊,另一種是使用 ThreadLocal改造 Canvas類型。
由于本篇著重介紹 ThreadLocal, 故下邊我們通過第二種方式解決上述問題。
修改 Canvas類為如下:
public class Canvas { private ThreadLocalmap = new ThreadLocal(); public String getContent() { return map.get(); } public void setContent(String content) { map.set(content); } }
啟動(dòng)執(zhí)行類,運(yùn)行結(jié)果如下:
小強(qiáng)在畫板繪制紅色 狗蛋在畫板繪制藍(lán)色 旺財(cái)在畫板繪制黃色
結(jié)果正常輸出。
2.3 ThreadLocal 與 synchronized 區(qū)別
名稱 | 原理 | 側(cè)重點(diǎn) |
---|---|---|
ThreadLocal | 空間換時(shí)間,每個(gè)線程都都提供一份變量副本,從而實(shí)現(xiàn)同時(shí)訪問而不相互干擾 | 多線程之間資源相互隔離 |
synchronized | 時(shí)間換空間,只提供一個(gè)變量,讓線程排隊(duì)訪問 | 多線程之間共享資源,同步訪問 |
三、ThreadLocal 內(nèi)部結(jié)構(gòu)
在看源碼之前,我們可以試著猜測 ThreadLocal內(nèi)部結(jié)構(gòu)是怎樣的。
比如,ThreadLocal內(nèi)部定義了一個(gè) Map容器。當(dāng)調(diào)用 ThreadLocal實(shí)例的 set方法時(shí),以當(dāng)前線程名/當(dāng)前線程實(shí)例作為 key, 需要保存的內(nèi)容作為 value 進(jìn)行操作。當(dāng)調(diào)用 get方式時(shí),以當(dāng)前線程名/當(dāng)前線程實(shí)例作為 key 獲取數(shù)據(jù)。
上述方案看似可以正常實(shí)現(xiàn)功能,實(shí)則存在一些問題:
1) 由 ThreadLocal 維護(hù) key-value 容器,當(dāng)線程增多并調(diào)用 ThreadLocal 實(shí)例 的set 方法時(shí),key-value 容器也隨之增大,即內(nèi)存占用也隨之增大。 2) 當(dāng)調(diào)用 ThreadLocal 實(shí)例方法的對象為線程池中的線程時(shí),無法區(qū)分線程是否被循環(huán)使用,即當(dāng)前線程之前已從線程池中被拿出調(diào)用 ThreadLocal 實(shí)例的 set 方法,如果當(dāng)前調(diào)用 get 方法就會(huì)取出之前的數(shù)據(jù)造成數(shù)據(jù)污染等問題。
那么,ThreadLocal內(nèi)部到底是怎么實(shí)現(xiàn)線程間內(nèi)部變量的隔離性的呢?
如上圖,由 Thread實(shí)例內(nèi)部維護(hù)名為 ThreadLocalMap的容器,其元素是以 ThreadLocal實(shí)例為 key ,保存對象作為 value 的數(shù)據(jù)結(jié)構(gòu),與我們猜測的實(shí)現(xiàn)方式相反。
對比我們之前設(shè)想的方案,JDK 實(shí)現(xiàn)方案有 2 個(gè)好處:
1) Map 存儲(chǔ)的 Entry 數(shù)量變少 2) 當(dāng)線程銷毀時(shí),ThreadLocalMap 也隨之銷毀,減少內(nèi)存使用
四、源碼分析
4.1 ThreadLocal 源碼
我們針對常用的 set、get、remove方法進(jìn)行源碼剖析。
public void set(T value) { // 獲取當(dāng)前線程對象 Thread t = Thread.currentThread(); // 獲取當(dāng)前線程對象維護(hù)的 ThreadLocalMap 對象 ThreadLocalMap map = getMap(t); if (map != null) // 如果 map 存在設(shè)置 entry map.set(this, value); else // 如果 map 不存在,由于 threadLocal 實(shí)例幫忙創(chuàng)建并綁定數(shù)據(jù) createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
set 方法執(zhí)行流程:
1) 獲取當(dāng)前線程對象 2) 通過當(dāng)前線程對象獲取 ThreadLocalMap 對象 3) 如果 ThreadLocalMap 對象存在,則將入?yún)⒃O(shè)置進(jìn) ThreadLocalMap 對象中 4) 如果 ThreadLocalMap 對象不存在,則給當(dāng)前線程創(chuàng)建 ThreadLocalMap 對象并設(shè)置入?yún)?/pre>public T get() { // 獲取當(dāng)前線程對象 Thread t = Thread.currentThread(); // 獲取當(dāng)前線程對象維護(hù)的 ThreadLocalMap 對象 ThreadLocalMap map = getMap(t); if (map != null) { // 如果 map 不為空,以當(dāng)前的 ThreadLocal 實(shí)例為 key, 獲取數(shù)據(jù) ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 如果 map 為空,初始化值,通常為 null return setInitialValue(); } 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; } protected T initialValue() { return null; }get 方法執(zhí)行流程:
1) 獲取當(dāng)前線程對象 2) 通過當(dāng)前線程對象獲取 ThreadLocalMap 對象 3) 如果 ThreadLocalMap 對象存在,則以當(dāng)前的 ThreadLocal 實(shí)例為 key, 獲取數(shù)據(jù) 4) 如果 ThreadLocalMap 對象不存在,則通過 initialValue 方法初始化 value 值。public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }remove 方法執(zhí)行流程:
1) 通過當(dāng)前線程對象獲取 ThreadLocalMap 對象 2) 如果 ThreadLocalMap 對象存在,則以當(dāng)前的 ThreadLocal 實(shí)例為 key, 進(jìn)行數(shù)據(jù)刪除4.2 ThreadLocalMap 源碼
ThreadLocalMap是 ThreadLocal的內(nèi)部類,其沒有實(shí)現(xiàn) Map接口,單獨(dú)實(shí)現(xiàn)了 Map的功能。
成員變量:
/** * 初始容量,必須是 2 的整次冪 */ private static final int INITIAL_CAPACITY = 16; /** * 存放數(shù)據(jù)的 table,數(shù)據(jù)長度也是 2 的整次冪 */ private Entry[] table; /** * 數(shù)組中 entry 的個(gè)數(shù) */ private int size = 0; /** * 進(jìn)行擴(kuò)展的閥值 */ private int threshold; // Default to 0Entry 內(nèi)部類:
static class Entry extends WeakReference> { Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } } Entry繼承 WeakReference類,也就是 key 是弱引用,其目的是將 ThreadLocal對象的生命周期與線程的生命周期解綁。
五、內(nèi)存泄漏
雖然 ThreadLocal作為弱引用 key 來使用,但是在某些情況下還是會(huì)造成內(nèi)存泄漏問題。 在分析內(nèi)存泄漏之前,我們先補(bǔ)充幾個(gè)概念:
內(nèi)存溢出:沒有足夠的內(nèi)存供申請者使用 內(nèi)存泄漏:程序中已動(dòng)態(tài)分配的堆內(nèi)存由于某種原因未釋放或無法釋放,造成系統(tǒng)內(nèi)存浪費(fèi),導(dǎo)致程序運(yùn)行速度減慢甚至系統(tǒng)崩潰等嚴(yán)重后果,該問題最終會(huì)導(dǎo)致內(nèi)存溢出 強(qiáng)引用:常見的對象引用,只要還有強(qiáng)引用指向一個(gè)對象,表明對象還“活著”,垃圾回收器就不會(huì)回收該對象 弱引用:垃圾回收期一旦發(fā)現(xiàn)只具有弱引用指向的對象,不管當(dāng)前內(nèi)存空間是否足夠,都會(huì)回收該對象了解了基本概念,接下來我們分析使用 ThreadLocal出現(xiàn)內(nèi)存泄漏的情況:
上圖為一個(gè)線程使用 ThreacLocal時(shí)的內(nèi)存結(jié)構(gòu)圖,實(shí)線箭頭表示強(qiáng)引用,虛線箭頭表示弱引用。
當(dāng) ThreadLocal 使用結(jié)束,棧內(nèi)存的 ThreadLocal 引用被回收,即引用 1 不再指向 ThreadLocal 對象。 由于引用 2 是弱引用,沒有任何強(qiáng)引用指向 ThreadLocal 對象,因此 ThreadLocal 對象會(huì)被 GC 回收,此時(shí) Entry 的 key = null 如果我們沒有會(huì)手動(dòng)刪除 Entry 對象,且當(dāng)前線程一直在運(yùn)行中,會(huì)存在一個(gè)強(qiáng)引用鏈 Thread 引用-> Thread 對象-> ThreadLocal 對象-> Entry 對象 -> Value,由于 value 不會(huì)被回收,而 key 又為 null, value 這塊內(nèi)存就永遠(yuǎn)無法被訪問,這就造成了內(nèi)存泄漏,既然使用弱引用作為 ThreadLocalMap的 key 會(huì)造成內(nèi)存泄漏,那為什么還要使用它呢?
其實(shí),在 ThreadLocalMap的 set、getEntry方法中,會(huì)對 key 為 null 進(jìn)行判斷,如果為 null, 那么會(huì)將 value 也設(shè)置為 null。
換言之,在使用 ThreadLocal的線程依然運(yùn)行的情況下,我們忘記調(diào)用 remove方法,弱引用比強(qiáng)引用多一層保障。弱引用指向的 ThreadLocal對象被回收,對應(yīng)的 value 在 TheadLocalMap調(diào)用 set、getEntry、remove任一方法時(shí)被設(shè)置為 null, 避免內(nèi)存泄漏。
六、總結(jié)
適用于多線程并發(fā)場景 使用 ThreadLocal 在同一線程,不同組件中可傳遞公共變量 每個(gè)線程的變量都是相互獨(dú)立,互不影響“如何掌握ThreadLocal的相關(guān)知識點(diǎn)”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!
本文題目:如何掌握ThreadLocal的相關(guān)知識點(diǎn)
標(biāo)題鏈接:http://www.xueling.net.cn/article/ghcosd.html