一文搞懂ThreadLocal及相关的内存泄露问题
首先,看一張整體的結構圖,來幫助理解
什么是ThreadLocal
ThreadLocal用于創建線程局部變量,如果創建一個ThreadLocal變量,那么訪問這個變量的每個線程都會有這個變量的一個副本,在實際多線程操作的時候,操作的是自己本地內存中的變量,從而規避了線程安全問題
ThreadLocal的簡單使用
package test;public class ThreadLocalTest {static ThreadLocal<String> threadLocal= new ThreadLocal<>();public static void main(String[] args) {Thread t1 = new Thread(()->{//設置線程1的本地變量的值threadLocal.set("線程1");System.out.println(threadLocal.get());threadLocal.remove();System.out.println("after remove : " + threadLocal.get());});Thread t2 = new Thread(()->{//設置線程2的本地變量threadLocal.set("線程2");System.out.println(threadLocal.get());threadLocal.remove();System.out.println("after remove : " + threadLocal.get());});t1.start();t2.start();} }輸出結果:
由此可知,雖然threadLocal為成員變量,即線程共享,但使用threadLocal在不用線程進行賦值、獲取等操作時,不同線程的操作互不相擾。
那么ThreadLocal是如何作為成員變量,卻能在實現多線程下數據不共享,作為線程局部變量?
ThreadLocal的原理
回到一開始的圖:
- ThreadLocal并不實際存儲數據,而是作為一個工具類,提供get、set方法根據當前線程用于操作當前線程中的實際數據
- 每個Thread(線程)中都持有一個ThreadLocal.ThreadLocalMap類型的成員變量,初始值為null。ThreadLocal的get、set方法實際上就是在操作這個成員變量。
- ThreadLocalMap持有一個Entry[]類型的成員變量table,類似JDK1.7的HashMap中的Entry[] table 可以類比學習
- Entry key為ThreadLocal<?>類型的的弱引用,value為Object類型的強引用
ThreadLocal的set()
public void set(T value) {//1、獲取當前線程(調用者線程)Thread t = Thread.currentThread();//2、獲取當前線程的ThreadLocalMap變量ThreadLocalMap map = getMap(t);//3、如果map不為null,就直接添加本地變量,key為當前定義的ThreadLocal變量的this引用,值為添加的本地變量值if (map != null)map.set(this, value);//4、如果map為null,說明首次添加,需要首先創建出對應的mapelsecreateMap(t, value); }ThreadLocalMap getMap(Thread t) {//獲取線程的threadLocalsreturn t.threadLocals; }void createMap(Thread t, T firstValue) {//創建線程的threadLocals, this為ThreadLocal<?>的引用t.threadLocals = new ThreadLocalMap(this, firstValue); }ThreadLocal的get()
在get方法的實現中,首先獲取當前調用者線程,如果當前線程的threadLocals不為null,就直接返回當前線程綁定的本地變量值,否則執行setInitialValue方法初始化threadLocals變量。在setInitialValue方法中,類似于set方法的實現,都是判斷當前線程的threadLocals變量是否為null,是則添加本地變量(這個時候由于是初始化,所以添加的值為null),否則創建threadLocals變量,同樣添加的值為null。
引用自:https://www.cnblogs.com/fsmly/p/11020641.html#_label0
ThreadLocal操作總結
根據上述源碼的分析可知:ThreadLocal最終操作的都是調用線程的ThreadLocalMap成員變量,因此不同線程使用同一個ThreadLocal成員變量互不相擾。
每個線程內部有一個ThreadLocal.ThreadLocalMap類型threadLocals的成員變量,該變量的類型為類似于HashMap,其中的key為當前定義的ThreadLocal變量的this引用,value為使用set方法設置的值。每個線程的本地變量存放在自己的本地內存變量threadLocals中,如果當前線程一直不消亡,那么這些本地變量就會一直存在(所以可能會導致內存溢出),因此使用完畢需要將其remove掉。
ThreadLocal使用不當造成內存泄漏問題
弱引用
弱引用(這里討論ThreadLocalMap中的Entry類的重點):如果一個對象只具有弱引用,那么這個對象就會被垃圾回收器GC掉(被弱引用所引用的對象只能生存到下一次GC之前,當發生GC時候,無論當前內存是否足夠,弱引用所引用的對象都會被回收掉)。弱引用也是和一個引用隊列聯合使用,如果弱引用的對象被垃圾回收期回收掉,JVM會將這個引用加入到與之關聯的引用隊列中。若引用的對象可以通過弱引用的get方法得到,當引用的對象唄回收掉之后,再調用get方法就會返回null
ThreadLocalMap內部實際上是一個Entry數組:
- Entry是繼承自WeakReference(弱引用)的一個類。
- Entry的key是指向ThreadLocal的弱引用,value一般為強引用
當一個線程調用ThreadLocal的set方法設置變量的時候,當前線程的ThreadLocalMap就會存放一個記錄(Entry),這個記錄的key值為ThreadLocal的弱引用,value就是通過set設置的值。
如果當前線程一直存在且沒有調用該ThreadLocal的remove方法,此時存在兩種情況:
- ThreadLocalMap之外存在ThreadLocal的引用:那么當前線程中的ThreadLocalMap中會存在對ThreadLocal變量的引用和value對象的引用,無法進行垃圾回收,導致這些本地變量一直存在,可能會出現內存溢出
- ThreadLocalMap之外不存在ThreadLocal的引用:因為ThreadLocalMap中的Entry的key使用的是ThreadLocal對象的弱引用,所以下一次垃圾回收時ThreadLocal(key)將被回收。此時ThreadLocalMap中就可能存在key為null但是value不為null的現象,出現內存泄漏。
因此每次使用完ThreadLocal,建議調用它的remove()方法,清除數據,避免內存泄漏
內存泄漏問題總結
ThreadLocalMap中的Entry的key使用的是ThreadLocal對象的弱引用,在沒有其他地方對ThreadLocal存在依賴時,ThreadLocalMap中的ThreadLocal對象(即key)就會被回收,而如果其他地方存在ThreadLocal的引用則不會被回收。當key被回收時,Map中就可能存在key為null但是value不為null的現象,出現內存泄漏。
因此每次使用完ThreadLocal,建議調用它的remove()方法,清除數據,避免內存泄漏。
參考
- ThreadLocal
- Java中的ThreadLocal詳解
- TheadLocal 引起的內存泄露故障分析
總結
以上是生活随笔為你收集整理的一文搞懂ThreadLocal及相关的内存泄露问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 华为手机电池不耐用怎么办
- 下一篇: 为什么苹果7手机打电话听筒有杂音