您好,登錄后才能下訂單哦!
本篇內容介紹了“如何掌握ThreadLocal的相關知識點”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
根據 Java 官方文檔的描述,我們可知 ThreadLocal 類用于提供線程內部的局部變量,其在多線程環境下能保證各個線程內部變量的隔離性。
換言之,ThreadLocal 提供線程內的局部變量,不同線程之間不會相互干擾,該變量作用范圍貫穿線程的生命周期,減少同一線程內多個方法或組件之間一些公共變量傳遞的復雜度。
返回值 | 方法名 | 描述 |
---|---|---|
T | get() | 返回此線程局部變量的當前線程副本中的值 |
void | remove() | 移除此線程局部變量當前線程的值 |
void | set(T value) | 將此線程局部變量的當前線程副本中的值設置為指定值 |
需求:用 3 名畫家在一個畫布上各自繪制一種顏色,并打印出其繪制的顏色。
/** * 畫布類 */ 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()); } } /** * 啟動類 */ public class Demo { public static void main(String[] args) { // 創建畫布 Canvas canvas = new Canvas(); Painter painter1 = new Painter("小強", canvas, "紅色"); Painter painter2 = new Painter("旺財", canvas, "黃色"); Painter painter3 = new Painter("狗蛋", canvas, "藍色"); painter1.start(); painter2.start(); painter3.start(); } }
執行結果如下:
小強在畫板繪制藍色 旺財在畫板繪制黃色 狗蛋在畫板繪制黃色
顯然,在多線程訪問同一個資源(畫布)的情況下,輸出結果出現并發問題。
現有 2 種解決方案:一種是在 run 方法中加入 synchronized 同步代碼塊,另一種是使用 ThreadLocal 改造 Canvas 類型。
由于本篇著重介紹 ThreadLocal, 故下邊我們通過第二種方式解決上述問題。
修改 Canvas 類為如下:
public class Canvas { private ThreadLocal<String> map = new ThreadLocal(); public String getContent() { return map.get(); } public void setContent(String content) { map.set(content); } }
啟動執行類,運行結果如下:
小強在畫板繪制紅色 狗蛋在畫板繪制藍色 旺財在畫板繪制黃色
結果正常輸出。
名稱 | 原理 | 側重點 |
---|---|---|
ThreadLocal | 空間換時間,每個線程都都提供一份變量副本,從而實現同時訪問而不相互干擾 | 多線程之間資源相互隔離 |
synchronized | 時間換空間,只提供一個變量,讓線程排隊訪問 | 多線程之間共享資源,同步訪問 |
在看源碼之前,我們可以試著猜測 ThreadLocal 內部結構是怎樣的。
比如,ThreadLocal 內部定義了一個 Map 容器。當調用 ThreadLocal 實例的 set 方法時,以當前線程名/當前線程實例作為 key, 需要保存的內容作為 value 進行操作。當調用 get 方式時,以當前線程名/當前線程實例作為 key 獲取數據。
上述方案看似可以正常實現功能,實則存在一些問題:
1) 由 ThreadLocal 維護 key-value 容器,當線程增多并調用 ThreadLocal 實例 的set 方法時,key-value 容器也隨之增大,即內存占用也隨之增大。 2) 當調用 ThreadLocal 實例方法的對象為線程池中的線程時,無法區分線程是否被循環使用,即當前線程之前已從線程池中被拿出調用 ThreadLocal 實例的 set 方法,如果當前調用 get 方法就會取出之前的數據造成數據污染等問題。
那么,ThreadLocal 內部到底是怎么實現線程間內部變量的隔離性的呢?
如上圖,由 Thread 實例內部維護名為 ThreadLocalMap 的容器,其元素是以 ThreadLocal 實例為 key ,保存對象作為 value 的數據結構,與我們猜測的實現方式相反。
對比我們之前設想的方案,JDK 實現方案有 2 個好處:
1) Map 存儲的 Entry 數量變少 2) 當線程銷毀時,ThreadLocalMap 也隨之銷毀,減少內存使用
我們針對常用的 set、get、remove 方法進行源碼剖析。
public void set(T value) { // 獲取當前線程對象 Thread t = Thread.currentThread(); // 獲取當前線程對象維護的 ThreadLocalMap 對象 ThreadLocalMap map = getMap(t); if (map != null) // 如果 map 存在設置 entry map.set(this, value); else // 如果 map 不存在,由于 threadLocal 實例幫忙創建并綁定數據 createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
set 方法執行流程:
1) 獲取當前線程對象 2) 通過當前線程對象獲取 ThreadLocalMap 對象 3) 如果 ThreadLocalMap 對象存在,則將入參設置進 ThreadLocalMap 對象中 4) 如果 ThreadLocalMap 對象不存在,則給當前線程創建 ThreadLocalMap 對象并設置入參
<hr>
public T get() { // 獲取當前線程對象 Thread t = Thread.currentThread(); // 獲取當前線程對象維護的 ThreadLocalMap 對象 ThreadLocalMap map = getMap(t); if (map != null) { // 如果 map 不為空,以當前的 ThreadLocal 實例為 key, 獲取數據 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 方法執行流程:
1) 獲取當前線程對象 2) 通過當前線程對象獲取 ThreadLocalMap 對象 3) 如果 ThreadLocalMap 對象存在,則以當前的 ThreadLocal 實例為 key, 獲取數據 4) 如果 ThreadLocalMap 對象不存在,則通過 initialValue 方法初始化 value 值。
<hr>
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
remove 方法執行流程:
1) 通過當前線程對象獲取 ThreadLocalMap 對象 2) 如果 ThreadLocalMap 對象存在,則以當前的 ThreadLocal 實例為 key, 進行數據刪除
ThreadLocalMap 是 ThreadLocal 的內部類,其沒有實現 Map 接口,單獨實現了 Map 的功能。
成員變量:
/** * 初始容量,必須是 2 的整次冪 */ private static final int INITIAL_CAPACITY = 16; /** * 存放數據的 table,數據長度也是 2 的整次冪 */ private Entry[] table; /** * 數組中 entry 的個數 */ private int size = 0; /** * 進行擴展的閥值 */ private int threshold; // Default to 0
Entry 內部類:
static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
Entry 繼承 WeakReference 類,也就是 key 是弱引用,其目的是將 ThreadLocal 對象的生命周期與線程的生命周期解綁。
雖然 ThreadLocal 作為弱引用 key 來使用,但是在某些情況下還是會造成內存泄漏問題。 在分析內存泄漏之前,我們先補充幾個概念:
內存溢出:沒有足夠的內存供申請者使用 內存泄漏:程序中已動態分配的堆內存由于某種原因未釋放或無法釋放,造成系統內存浪費,導致程序運行速度減慢甚至系統崩潰等嚴重后果,該問題最終會導致內存溢出 強引用:常見的對象引用,只要還有強引用指向一個對象,表明對象還“活著”,垃圾回收器就不會回收該對象 弱引用:垃圾回收期一旦發現只具有弱引用指向的對象,不管當前內存空間是否足夠,都會回收該對象
了解了基本概念,接下來我們分析使用 ThreadLocal 出現內存泄漏的情況:
上圖為一個線程使用 ThreacLocal 時的內存結構圖,實線箭頭表示強引用,虛線箭頭表示弱引用。
當 ThreadLocal 使用結束,棧內存的 ThreadLocal 引用被回收,即引用 1 不再指向 ThreadLocal 對象。 由于引用 2 是弱引用,沒有任何強引用指向 ThreadLocal 對象,因此 ThreadLocal 對象會被 GC 回收,此時 Entry 的 key = null 如果我們沒有會手動刪除 Entry 對象,且當前線程一直在運行中,會存在一個強引用鏈 Thread 引用-> Thread 對象-> ThreadLocal 對象-> Entry 對象 -> Value,由于 value 不會被回收,而 key 又為 null, value 這塊內存就永遠無法被訪問,這就造成了內存泄漏,
既然使用弱引用作為 ThreadLocalMap 的 key 會造成內存泄漏,那為什么還要使用它呢?
其實,在 ThreadLocalMap 的 set、getEntry 方法中,會對 key 為 null 進行判斷,如果為 null, 那么會將 value 也設置為 null。
換言之,在使用 ThreadLocal 的線程依然運行的情況下,我們忘記調用 remove 方法,弱引用比強引用多一層保障。弱引用指向的 ThreadLocal 對象被回收,對應的 value 在 TheadLocalMap 調用 set、getEntry、remove 任一方法時被設置為 null, 避免內存泄漏。
適用于多線程并發場景 使用 ThreadLocal 在同一線程,不同組件中可傳遞公共變量 每個線程的變量都是相互獨立,互不影響
“如何掌握ThreadLocal的相關知識點”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。