ThreadLocal技術分享

學習一個東西首先要知道爲什麼要引入它,就是我們能用它來幹什麼。所以我們先來看看ThreadLocal對我們到底有什麼用,然後再來看看它的實現原理。

ThreadLocal如果單純從名字上來看像是“本地線程"這麼個意思,只能說這個名字起的確實不太好,很容易讓人產生誤解,ThreadLocalVariable(線程本地變量)應該是個更好的名字。我們先看一下官方對ThreadLocal的描述:

該類提供了線程局部 (thread-local) 變量。這些變量不同於它們的普通對應物,因爲訪問某個變量(通過其 get 或 set 方法)的每個線程都有自己的局部變量,它獨立於變量的初始化副本。ThreadLocal 實例通常是類中的 private static 字段,它們希望將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。

1、每個線程都有自己的局部變量

每個線程都有一個獨立於其他線程的上下文來保存這個變量,一個線程的本地變量對其他線程是不可見的(有前提,後面解釋)

2、獨立於變量的初始化副本

ThreadLocal可以給一個初始值,而每個線程都會獲得這個初始化值的一個副本,這樣才能保證不同的線程都有一份拷貝。

3、狀態與某一個線程相關聯

ThreadLocal 不是用於解決共享變量的問題的,不是爲了協調線程同步而存在,而是爲了方便每個線程處理自己的狀態而引入的一個機制,理解這點對正確使用ThreadLocal至關重要

什麼時候用用到:

舉幾個例子說明一下:

1、比如線程中處理一個非常複雜的業務,可能方法有很多,那麼,使用 ThreadLocal 可以代替一些參數的顯式傳遞;

2、比如用來存儲用戶 Session。Session 的特性很適合 ThreadLocal ,因爲 Session 之前當前會話週期內有效,會話結束便銷燬。我們先籠統但不正確的分析一次 web 請求的過程:

  • 用戶在瀏覽器中訪問 web 頁面;
  • 瀏覽器向服務器發起請求;
  • 服務器上的服務處理程序(例如tomcat)接收請求,並開啓一個線程處理請求,期間會使用到 Session ;
  • 最後服務器將請求結果返回給客戶端瀏覽器。

從這個簡單的訪問過程我們看到正好這個 Session 是在處理一個用戶會話過程中產生並使用的,如果單純的理解一個用戶的一次會話對應服務端一個獨立的處理線程,那用 ThreadLocal 在存儲 Session ,簡直是再合適不過了。但是例如 tomcat 這類的服務器軟件都是採用了線程池技術的,並不是嚴格意義上的一個會話對應一個線程。並不是說這種情況就不適合 ThreadLocal 了,而是要在每次請求進來時先清理掉之前的 Session ,一般可以用攔截器、過濾器來實現。

3、在一些多線程的情況下,如果用線程同步的方式,當併發比較高的時候會影響性能,可以改爲 ThreadLocal 的方式,例如高性能序列化框架 Kyro 就要用 ThreadLocal 來保證高性能和線程安全;

4、還有像線程內上線文管理器、數據庫連接等可以用到 ThreadLocal;

現在我們先來看一段代碼:

image

運行結果:

image

這個例子告訴我們 每一個線程之間的變量是互相之間不影響。

接着我們再來看一個例子:

image

輸出結果:

image

咦,爲什麼這個每一個數值不一樣呢。不是說好的 互不影響嗎?

這時候就要拿出我多久不動的畫筆,來給你們解析下爲什麼會出現這個情況。

我們先來看下一下 這個Demo1和Demo2的區別。

Demo1:

image

Demo2:

image

問題來了,Demo1每一次返回的都是0一個基本類型。但是indexnum是一個對象。所以每一次指向的還是同一個對象,爲了加深理解 我們畫一幅圖來表示下。

image

所以ThreadLocal只保存了對象的地址副本,我們初始化的對象都是指向同一個地址,所以就會有這樣子的 問題。那我們怎麼解決這個問題呢。其實很簡單

image

只需要每一次new 一個新的對象就好了,這樣子就不會指向同一個地址。

再來看輸出結果:

image

接下來我們看一下內部的源碼 小戴帶你讀 ThreadLocal源碼

image

ThreadLocal裏面有幾個方法 最主要的就是

public T get() { }

public  void set(T value) { }

public  void remove() { }

protected T initialValue() { }

get()方法是用來獲取ThreadLocal在當前線程中保存的變量副本,set()用來設置當前線程中變量的副本,remove()用來移除當前線程中變量的副本,initialValue()是一個protected方法,一般是用來在使用時進行重寫的,它是一個延遲加載方法

1:get方法:

image

  1. 先獲取到當先的線程
  2. 判斷當前線程中是否包含了ThreadLocalMap
  3. map 是 null 或者 map中的本地變量是空的話 就去創建初始值

image

創建初始值的時候又再去判斷map是否存在,

  1. 不存在的話初始化map並setmap的key和value
  2. 存在的話直接set map的key和value

image

這就是ThreadLocal的get操作。

2:set方法:

image

也很類似 先獲取當前線程,然後從當前線程的ThreadLocalMap中set值 存在則直接set不存在則初始化並set值

3:remove方法:

image

獲取當前線程,當前線程實例就是該map的key 獲取並remove掉。

這個就是ThreadLocal常用的三個方法。

接下去我們再來了解下ThreadLocal中的ThreadLocalMap

image

通過之前的分析已經知道,當使用ThreadLocal保存一個value時,會在ThreadLocalMap中的數組插入一個Entry對象,按理說key-value都應該以強引用保存在Entry對象中,但在ThreadLocalMap的實現中,key被保存到了WeakReference對象中。

這就導致了一個問題,ThreadLocal在沒有外部強引用時,發生GC時會被回收,如果創建ThreadLocal的線程一直持續運行,那麼這個Entry對象中的value就有可能一直得不到回收,發生內存泄露。

image

那有人會問爲什麼要使用弱引用,其實這跟java 的設計思想有關係,java一直提倡的是弱化指針管理。所以就採用了key是若引用。那我們來對比下兩種情況

  1. key 使用強引用:引用的ThreadLocal的對象被回收了,但是ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除,ThreadLocal不會被回收,導致Entry內存泄漏。
  2. key 使用弱引用:引用的ThreadLocal的對象被回收了,由於ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal也會被回收。value在下一次ThreadLocalMap調用set,get,remove的時候會被清除。

比較兩種情況,我們可以發現:由於ThreadLocalMap的生命週期跟Thread一樣長,如果都沒有手動刪除對應key,都會導致內存泄漏,但是使用弱引用可以多一層保障:弱引用ThreadLocal不會內存泄漏,對應的value在下一次ThreadLocalMap調用set,get,remove的時候會被清除。

因此,ThreadLocal內存泄漏的根源是:由於ThreadLocalMap的生命週期跟Thread一樣長,如果沒有手動刪除對應key就會導致內存泄漏,而不是因爲弱引用引起的。

綜上所述

我們再使用ThredLocal的時候,使用完畢都要調用下remove方法。清除數據。

血淚教訓!!!

p3事故

還記得那時3月26號的那一天,我不會忘記。那天中午睡醒之後,小夥伴帶着我去luckin coffee buy a bottle of coffee,美滋滋。又是一個喝着咖啡敲着代碼的日誌。這時候內部技術羣突然反饋,商家登錄信息串號了。透,腦瓜子嗡嗡的~這個時候我在想我們也沒有改動代碼爲什麼會出現這樣的問題呢。

這個時候,我意識到問題的嚴重性,馬上飛奔回辦公室,此時故障時間已經超過5分鐘。我打開ci界面,馬上回滾,還好merchant發佈的快,5分鐘內都回滾完畢。通知運營羣讓商家刷新頁面,故障得以恢復。

其實在回滾的時候我已經意識到問題出在哪裏(ThreadLocal使用後沒有清除),隨即出現在我腦海裏的是,爲啥以前沒有出現問題現在就導致了這個問題的發生。後定位到 spring包版本的升級導致了aop的執行順序:

image

我們代碼裏做了一個兜底方案,就是通過aop都去刪除當前使用的ThreadLocal信息。但是spring包版本升級之後,aop先執行了這個兜底方案,然後又去執行了其他使用ThreadLocal的場景。

導致清除完之後,又去執行了Get操作,導致其他用戶訪問,拿到了其他線程池裏面的用戶信息(tomcat線程池複用)

後續針對這種情況做了兜底方案的改正。通過filter去實現兜底方案 因爲執行順序是

before:

aop 沒有指定order所以沒有辦法處理。

after:

最外設置一個filter 裏面設置和移除ThreadLocal

image

image

在最外層配置filter,ThreadLocal設置只放在Filter裏面進行,方法執行完畢後,在filter中清除ThreadLocal。

ps:不管有沒有這層兜底,使用了ThreadLocal之後 都在在final代碼塊中revome掉。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章