线上系统因为一个ThreadLocal直接内存飙升
前言
大家對于ThreadLocal這一個都應該聽說過的吧,不知道大家對于這個掌握的怎么樣了已經(jīng)
這不,我那愛學習的表妹不知道又從哪里聽來了這個技術點,回家就得意洋洋的給我說,表哥,我今天又學會了一個技術點ThreadLocal
哦,不錯啊
你你這態(tài)度,好像不太信的樣子啊,表妹咬牙切齒的說著
沒沒沒,我信。我表妹那么聰明伶俐,肯定會
不行,你這態(tài)度太敷衍了,不信我給你講一遍
得,你也先別給我講了,你把你的Mac拿過來,我給你寫個東西
接過她的Mac,我三下五除二給她寫了一個小例子
public class ThreadPool {private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10, 10, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 1000; ++i) {poolExecutor.execute(new Runnable() {@Overridepublic void run() {ThreadLocal<BigObject> threadLocal = new ThreadLocal<>();threadLocal.set(new BigObject());// 其他業(yè)務代碼}});Thread.sleep(1000);}}static class BigObject {// 100Mprivate byte[] bytes = new byte[100 * 1024 * 1024];} }你先看看這段代碼,給我說說你的理解
表妹眉頭一皺,你這是侮辱我的智商嗎,這不就是創(chuàng)建了一個線程池,然后使用for循環(huán)增加線程,往線程池里面提交一千個任務嗎
這也沒啥問題啊,每個任務會向ThreadLocal變量里面塞一個大對象,然后執(zhí)行其他業(yè)務邏輯
總之,看著沒啥大毛病,這就是表妹的結論
如果你覺得這段代碼沒啥問題,那看來你對ThreadLocal學的還是不夠徹底啊
代碼分析
來,我來給你透徹的說一遍,包教包會
先分析一下上面的代碼
創(chuàng)建一個核心線程數(shù)和最大線程數(shù)都為10的線程池,保證線程池里一直會有10個線程在運行。
使用for循環(huán)向線程池中提交了100個任務。
定義了一個ThreadLocal類型的變量,Value類型是大對象。
每個任務會向threadLocal變量里塞一個大對象,然后執(zhí)行其他業(yè)務邏輯。
由于沒有調用線程池的shutdown方法,線程池里的線程還是會在運行。
說個結論
上面的代碼會造成內存泄漏,會讓服務的內存一直很高,即使GC之后也不會降低太多,這不是我們想要的結果
?
ThreadLocal存儲模型
在ThreadLocal的內部有一個靜態(tài)內部類ThreadLocalMap,這個才是真正存儲對象的Map,我們平時使用的set存儲的值實際上是存儲到這里面的
我們重點來看set正如上面代碼所示,ThreadLocalMap的內部實際是一個Entry數(shù)組,而這個Entry對象就是Key和Value組成
重點來了,這個Key就是ThreadLocal實例本身,這個Value就是我們要存儲的真實的數(shù)據(jù)
大家看到這,是不是覺得很熟悉,沒錯,這個ThreadLocalMap就是一個Map而已,這個Map和普通的Map有兩點不同之處
1、Key、Value的存儲內容的不同
2、ThreadLocalMap的Key是一個弱引用類型
其實吧,第一點也不算是不同,只是這里存儲的Key有點出乎我們的意料,這里重點的重點其實是這個第二點,也就是這個弱引用類型,大家先記著,下面說
我們先來看一下ThreadLocal的get和set方法來驗證一下我的說法
ThreadLocal的set方法
public class ThreadLocal<T> {public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}// 省略其他方法 }set的邏輯其實也是很簡單的,獲取當前線程的ThreadLocalMap,然后就直接往map里面添加Key和Value,而這個Key就是this,這個this就是ThreadLocal實例本身了
value就是我們要存儲的數(shù)據(jù)
這里需要注意一下,map的獲取是需要從Thread類對象里面取,看一下Thread類的定義。
public class Thread implements Runnable {ThreadLocal.ThreadLocalMap threadLocals = null;//省略其他 }ThreadLocal的get方法
弱引用獲取當前線程的ThreadLocalMap實例,如果不為空,直接用當前ThreadLocal的實例來作為Key獲取Value即可
如果ThreadLocalMap為空,或者根據(jù)當前的ThreadLocal實例獲取到的Value為空,則執(zhí)行setInitialValue()
而setInitialValue的內部實現(xiàn)就是如果Map不為空,就設置鍵值對,為空,則創(chuàng)建Map
?
ThreadLocal的內部關系
這個圖畫的很清晰了
每個Thread線程會有一個threadlocals,這是一個ThreadLocalMap對象
通過這個對象,可以存儲線程的私有變量,就是通過ThreadLocal的set和get來操作
ThreadLocal本身不是一個容器,本身不存儲任何數(shù)據(jù),實際存儲數(shù)據(jù)的對象是ThreadLocalMap對象,操作的過程就類似于Map的put和get
這個ThreadLocalMap對象就是負責ThreadLocal真實存儲數(shù)據(jù)的對象,內部的存儲結構是Entry數(shù)組,這個Entry就是存儲Key和Value對象
Key就是ThreadLocal實例本身,而Value就是我們要存儲的真實數(shù)據(jù),而我們也從上面的源碼中看到了,存和取就是根據(jù)ThreadLocal實例來操作的
ThreadLocal內存模型
圖中左邊是棧,右邊是堆。線程的一些局部變量和引用使用的內存屬于Stack(棧)區(qū),而普通的對象是存儲在Heap(堆)區(qū)。
線程運行時,我們定義的TheadLocal對象被初始化,存儲在Heap,同時線程運行的棧區(qū)保存了指向該實例的引用,也就是圖中的ThreadLocalRef。
當ThreadLocal的set/get被調用時,虛擬機會根據(jù)當前線程的引用也就是CurrentThreadRef找到其對應在堆區(qū)的實例,然后查看其對用的TheadLocalMap實例是否被創(chuàng)建,如果沒有,則創(chuàng)建并初始化。
Map實例化之后,也就拿到了該ThreadLocalMap的句柄,那么就可以將當前ThreadLocal對象作為key,進行存取操作。
圖中的虛線,表示key對應ThreadLocal實例的引用是個弱引用。
四種引用
強引用,一直活著:
類似“Object obj=new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象實例。
弱引用,回收就會死亡
被弱引用關聯(lián)的對象實例只能生存到下一次垃圾收集發(fā)生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯(lián)的對象實例。在JDK 1.2之后,提供了WeakReference類來實現(xiàn)弱引用。
軟引用,有一次活的機會:
軟引用關聯(lián)著的對象,在系統(tǒng)將要發(fā)生內存溢出異常之前,將會把這些對象實例列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。在JDK 1.2之后,提供了SoftReference類來實現(xiàn)軟引用。
虛引用,也稱為幽靈引用或者幻影引用,它是最弱的一種引用關系。
一個對象實例是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯(lián)的唯一目的就是能在這個對象實例被收集器回收時收到一個系統(tǒng)通知。在JDK 1.2之后,提供了PhantomReference類來實現(xiàn)虛引用。
?
ThreadLocal中的弱引用和內存泄漏
static class ThreadLocalMap {// 定義一個Entry類,key是一個弱引用的ThreadLocal對象// 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的實現(xiàn),Key會被保存到弱引用WeakReference中
這里的Key作為弱引用是關鍵,我們分兩種情況來討論
Key作為強引用的時候
引用ThreadLocal的對象被回收了,但是ThreadLocalMap還有ThreadLocal的強引用,所以如果沒有進行手動刪除的話,ThreadLocal是不會被GC回收的,也就是會導致Entry的內存泄露
一句話,強引用的時候需要手動刪除才會釋放內存
Key作為弱引用的時候
引用ThreadLocal的對象被回收了之后,由于ThreadLocalMap持有的是ThreadLocal的弱引用,即使不會手動刪除這個ThreadLocal,這個ThreadLocal也會被回收
前提是該對象只被弱引用所關聯(lián),別的強引用關聯(lián)不到!
而Value則是在下一次調用get、set、remove的時候進行清除,才會被GC自動回收
一句話,弱引用是多一層屏障,無外部強引用的時候,弱引用ThreadLocal會被GC回收,但是該ThreadLocal對應的Value只有執(zhí)行set、get和remove的時候才會被清除
比較這兩種情況
由于ThreadLocalMap的生命周期是和Thread一樣的,因為它是Thread內部實現(xiàn)的,如果沒有手動刪除對應的key,都會導致內存泄漏
而ThreadLocal使用弱引用,會多了一層保障,ThreadLocal在被清理之后,也就是Map中的key會變成null,在使用對應value的時候就會將這個value進行清除
但是!但是!但是!
使用弱引用并不代表不需要考慮內存泄漏,只是多了一層屏障而已!
造成內存泄漏的根源就是:ThreadLocalMap和Thread的生命周期一樣長,如果沒有手動刪除對應key,就會導致相應的value不能及時得到清除,造成內存泄漏
我們在線上使用最多的就是線程池了,這樣問題就大了
你想啊,線程池里面有10個活躍線程,線程一直在運行,不會停止,每次線程直接拿到過來用,然后用完之后會再次放到線程池中,此時線程并不會停止
也就是說這些線程的每一次使用都有可能產生新的ThreadLocal,而我們使用完對應的ThreadLocal之后,如果不去手動執(zhí)行remove刪除相應的key,就會導致ThreadLocalMap中的Entry一直在增加,并且內存是永遠得不到釋放
這本身就是一個很恐怖的事情,再要是放到ThreadLocal中的對象還是超大對象,那后果不堪設想
?
如何避免內存泄漏
綜合上面的分析,我們可以理解ThreadLocal內存泄漏的前因后果,那么怎么避免內存泄漏呢?
答案就是:每次使用完ThreadLocal,建議調用它的remove()方法,清除數(shù)據(jù)。
另外需要強調的是并不是所有使用ThreadLocal的地方,都要在最后remove(),因為他們的生命周期可能是需要和項目的生存周期一樣長的,所以要進行恰當?shù)倪x擇,以免出現(xiàn)業(yè)務邏輯錯誤!
?
ThreadLocal的應用場景
場景1:
ThreadLocal 用作保存每個線程獨享的對象,為每個線程都創(chuàng)建一個副本,這樣每個線程都可以修改自己所擁有的副本, 而不會影響其他線程的副本,確保了線程安全。
場景2:
ThreadLocal 用作每個線程內需要獨立保存信息,以便供其他方法更方便地獲取該信息的場景。每個線程獲取到的信息可能都是不一樣的,前面執(zhí)行的方法保存了信息后,后續(xù)方法可以通過ThreadLocal 直接獲取到,避免了傳參,類似于全局變量的概念。
舉個具體的使用例子
比如可以用于保存線程不安全的工具類,典型的需要使用的類就是 SimpleDateFormat。
在這種情況下,每個Thread內都有自己的實例副本,且該副本只能由當前Thread訪問到并使用,相當于每個線程內部的本地變量,這也是ThreadLocal命名的含義。因為每個線程獨享副本,而不是公用的,所以不存在多線程間共享的問題。
這種線程不安全的工具類如果需要在很多的線程中同時使用的話,任務數(shù)量巨大的情況下,也就是需要線程數(shù)巨多的情況下,這個不安全我們就需要讓它變得安全
比如使用Synchronized鎖,這樣可以解決,但是Synchronized會讓線程進入一個排隊的狀態(tài),大大降低整體的工作效率
我們在線上一般使用線程池,ThreadLocal再合適不過了,ThreadLocal給每個線程維護一個自己的simpleDateFormat對象,這個對象在線程之間是獨立的,互相沒有關系的。這也就避免了線程安全問題。與此同時,simpleDateFormat對象還不會創(chuàng)造過多,線程池有多少個線程,所以需要多少個對象即可。
再說一個形象的場景
每個線程內需要保存類似于全局變量的信息(例如在攔截器中獲取的用戶信息),可以讓不同方法直接使用,避免參數(shù)傳遞的麻煩卻不想被多線程共享(因為不同線程獲取到的用戶信息不一樣)。
例如,用 ThreadLocal 保存一些業(yè)務內容(用戶權限信息、從用戶系統(tǒng)獲取到的用戶名、用戶ID 等),這些信息在同一個線程內相同,但是不同的線程使用的業(yè)務內容是不相同的。
在線程生命周期內,都通過這個靜態(tài) ThreadLocal 實例的 get() 方法取得自己 set 過的那個對象,避免了將這個對象(如 user 對象)作為參數(shù)傳遞的麻煩。
這其實就是類似于一種責任鏈的模式
有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
總結
以上是生活随笔為你收集整理的线上系统因为一个ThreadLocal直接内存飙升的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python在工厂中的应用_python
- 下一篇: Log4j未平,Logback 又起,再