史上最全最详细的ThreadLocal 使用
文章目錄
- ThreadLocal 簡介
- ThreadLocal 的四個方法
- ThreadLocal的核心機制
- ThreadLocal為什么會內存泄漏
- 如何避免泄漏
- 為什么ThreadLocalMap的key是弱引用呢?
- 錯誤使用ThreadLocal導致線程不安全
ThreadLocal 簡介
一種解決多線程環境下成員變量的問題的方案,但是與線程同步無關,其思路是為每一個線程創建一個單獨的變量副本,從而每個線程都可以獨立地改變所擁有的變量副本,而不會影響其他線程所對應的副本。
ThreadLocal不是用于解決共享變量的問題的,也不是為了協調線程同步而存在,而是為了方便每個線程處理自己的狀態而引入的一個機制。
ThreadLocal 的四個方法
void set(Object value)
設置當前線程的線程局部變量的值。
public Object get()
該方法返回當前線程所對應的線程局部變量。
public void remove()
將當前線程局部變量的值刪除,目的是為了減少內存的占用,該方法是JDK 5.0新增的方法。需要指出的是,當線程結束后,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量并不是必須的操作,但它可以加快內存回收的速度。
protected Object initialValue()
返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是為了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,并且僅執行1次。ThreadLocal中的缺省實現直接返回一個null。
public final static ThreadLocal RESOURCE = new ThreadLocal();
RESOURCE 代表一個能夠存放String類型的ThreadLocal對象。此時不論什么一個線程能夠并發訪問這個變量,對它進行寫入、讀取操作,都是線程安全的。
ThreadLocal的核心機制
- 每個Thread線程內部都有一個Map
- Map里面存儲線程本地對象(key)和線程的變量副本(value)
- 但是,Thread內部的Map是由ThreadLocal維護的,由ThreadLocal負責向map獲取和設置線程的變量值。
- 所以對于不同的線程,每次獲取副本值時,別的線程并不能獲取到當前線程的副本值,形成了副本的隔離,互不干擾。
下面我們講 ThreadLocalMap
ThreadLocalMap 是實現線程隔離機制的關鍵,每個 Thread 內部都有 一個ThreadLocal.ThreadLocalMap
類型的成員變量,該成員變量用來存儲實際的ThreadLocal變量副本。
提供了一種用健值對方式存儲每一個線程的變量副本的 方法,key為ThreadLocal對象,value則是對應線程的 變量副本。
public class Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null;}
可以看到有個Entry內部靜態類,它繼承了WeakReference,總之它記錄了兩個信息,一個是ThreadLocal類型,一個是Object類型的值。getEntry方法則是獲取某個ThreadLocal對應的值,set方法就是更新或賦值相應的ThreadLocal對應的值。
Entry繼承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用類型的,Value并非弱引用。
ThreadLocalMap 和 HashMap的區別?
ThreadLocalMap的問題
ThreadLocal的原理:每個Thread內部維護著一個ThreadLocalMap,它是一個Map。這個映射表的Key是一個弱引用,其實就是ThreadLocal本身,Value是真正存的線程變量Object。
也就是說ThreadLocal本身并不真正存儲線程的變量值,它只是一個工具,用來維護Thread內部的Map,幫助存和取。注意上圖的虛線,它代表一個弱引用類型,而弱引用的生命周期只能存活到下次GC前。
建議:
每個線程只存一個變量,這樣的話所有的線程存放到map中的Key都是相同的ThreadLocal,如果一個線程要保存多個變量,就需要創建多個ThreadLocal,多個ThreadLocal放入Map中時會極大的增加Hash沖突的可能。
set方法
- 獲取當前線程的成員變量map’
- map非空,則重新將ThreadLocal和新的value副本放入到map中。
- map空,則對線程的成員變量ThreadLocalMap進行初始化創建,并將ThreadLocal和value副本放入map中。
get 方法
remove()方法
ThreadLocal為什么會內存泄漏
ThreadLocal 在 ThreadLocalMap 中是以一個弱引用身份被Entry中的Key引用的,因此如果ThreadLocal沒有外部強引用來引用它,那么ThreadLocal會在下次JVM垃圾收集時被回收。這個時候就會出現Entry中Key已經被回收,出現一個null Key的情況,外部讀取ThreadLocalMap中的元素是無法通過null Key來找到Value的。因此如果當前線程的生命周期很長,一直存在,那么其內部的ThreadLocalMap對象也一直生存下來,這些null key就存在一條強引用鏈的關系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,這條強引用鏈會導致Entry不會回收,Value也不會回收,但Entry中的Key卻已經被回收的情況,造成內存泄漏。
但是JVM團隊已經考慮到這樣的情況,并做了一些措施來保證ThreadLocal盡量不會內存泄漏:在ThreadLocal的get()、set()、remove()方法調用的時候會清除掉線程ThreadLocalMap中所有Entry中Key為null的Value,并將整個Entry設置為null,利于下次內存由于ThreadLocalMap的key是弱引用,而Value是強引用。這就導致了一個問題,ThreadLocal在沒有外部對象強引用時,發生GC時弱引用Key會被回收,而Value不會回收,如果創建ThreadLocal的線程一直持續運行,那么這個Entry對象中的value就有可能一直得不到回收,發生內存泄露。
我們先看看ThreadLocal的get方法的底層實現
在調用map.getEntry(this)時,內部會判斷key是否為null,繼續看map.getEntry(this)源碼
在getEntry方法中,如果Entry中的key發現是null,會繼續調用getEntryAfterMiss(key, i, e)方法,其內部回做回收必要的設置,繼續看內部源碼:
注意k == null這里,繼續調用了expungeStaleEntry(i)方法,expunge的意思是擦除,刪除的意思,見名知意,在來看expungeStaleEntry方法的內部實現:
注意這里,將當前Entry刪除后,會繼續循環往下檢查是否有key為null的節點,如果有則一并刪除,防止內存泄漏。
如何避免泄漏
既然Key是弱引用,那么我們要做的事,就是在調用ThreadLocal的get()、set()方法時完成后再調用remove方法,將Entry節點和Map的引用關系移除,這樣整個Entry對象在GC Roots分析后就變成不可達了,下次GC的時候就可以被回收。
如果使用ThreadLocal的set方法之后,沒有顯示的調用remove方法,就有可能發生內存泄露,所以養成良好的編程習慣十分重要,使用完ThreadLocal之后,記得調用remove方法。
為什么ThreadLocalMap的key是弱引用呢?
錯誤使用ThreadLocal導致線程不安全
為什么每個線程都輸出5?難道他們沒有獨自保存自己的Number副本嗎?為什么其他線程還是能夠修改這個值?仔細考察ThreadLocal和Thead的代碼,我們發現ThreadLocalMap中保存的其實是對象的一個引用,這樣的話,當有其他線程對這個引用指向的對象實例做修改時,其實也同時影響了所有的線程持有的對象引用所指向的同一個對象實例。這也就是為什么上面的程序為什么會輸出一樣的結果:5個線程中保存的是同一Number對象的引用,在線程睡眠的時候,其他線程將num變量進行了修改,而修改的對象Number的實例是同一份,因此它們最終輸出的結果是相同的。而上面的程序要正常的工作,應該的用法是讓每個線程中的ThreadLocal都應該持有一個新的Number對象。
總結
以上是生活随笔為你收集整理的史上最全最详细的ThreadLocal 使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 快捷方式打不开
- 下一篇: Raptor-回文字符串判断