線程的私有領地 ThreadLocal

從名字上看,『ThreadLocal』可能會給你一種本地線程的概念印象,可能會讓你聯想到它是一個特殊的線程。java

但實際上,『ThreadLocal』卻營造了一種「線程本地變量」的概念,也就是說,同一個變量在每一個線程的內部,都有一份副本,且相互之間具備不一樣的取值。git

這樣的設計具備怎樣的應用場景呢?是怎麼樣的一種設計原理呢?程序員

別急,本篇就來詳細的探討探討它。github

基本介紹

上面咱們粗略的介紹了「什麼是 ThreadLocal ?」的這個問題,下面咱們來看看它的一個基本使用是什麼樣的,以及設計出來旨在解決什麼問題等相關內容。數組

咱們先看這麼一段程序:安全

image

函數 A 調用了函數 B,接着調用了函數 C、D,這麼深層次的調用體系在真實的業務場景下是很常見的。微信

可是假如我如今要對函數 D 中要打印的字符串進行動態的傳入,那你是否是得修改每個方法的形參列表,增長一個形參位,接着在函數 A 中的調用上傳入一個參數過來?多線程

這太繁瑣了,咱們使用 ThreadLocal 就能夠簡單解決這種「需求變動」的問題:併發

image

這一連串函數的調用必然是同一個線程調用的,那麼咱們只要在最開頭存儲下一個變量,不管當前線程調用了多少層函數,這個局部變量一直都存在。函數

這是 ThreadLocal 的一種使用場景,但有點低估它的價值了,ThreadLocal 最經常使用的使用場景是,在多線程併發情境下避免一些因爲共享變量競爭訪問致使的併發問題。

咱們來看看廣爲你們詬病的 SimpleDateFormat,周所周知,這是個多線程不安全的類,咱們再次回顧下之前的內容:

SimpleDateFormat 是一個用於格式化日期和字符串的工具類,主要有兩個核心方法,format 和 parse,前者用於將一個日期轉換成指定格式的字符串,後者用於將一個指定格式的字符串轉換成一個日期對象。

可是,這兩個方法都不是線程安全的,format 方法倒還好,最多致使傳入的 Date 格式化成錯誤的值,而 parse 將直接致使多種異常。緣由很簡單,他們公用了同一個局部變量。

image

format 方法的第一個行就是將傳入的 Date 對象保存到父類 DateFormat 的字段 calendar 上,而後會在後面邏輯中讀取這個 Date 實例並完成轉換字符串的邏輯。

可是徹底有可能在你設置完日期時間後,其餘線程也執行 format 方法並覆蓋了你的日期時間 calendar 中的值,這樣你後續的轉換字符串的動做基於的日期已經再也不是傳入的日期對象了,致使的最終結果就是錯誤將別人的日期 Date 轉換成字符串並返回了。

不信,你看這麼一段代碼:

image

執行後,我給你找一個錯誤的數據打印日誌:

image

明顯的是構造的上一個線程傳入的 Date 參數,也就是在格式化的過程當中被別的線程覆蓋了本身傳入的 Date 致使的錯誤的格式化數據。

parse 方法的線程不安全就不帶你們重現了,它更嚴重,由於方法內部會執行一個 clear 操做清空 calendar 字段保存的值,而且仍是非線程安全式的清空,會致使某些其餘線程發生轉換異常的,具體的你們能夠本身去看。

而咱們簡單的使用 ThreadLocal 就能夠解決上述 format 的線程不安全問題:

image

ThreadLocal 的 set 方法將致使每一個線程的內部都持有一個 SimpleDateFormat 的實例,本身用本身的,也就不存在由於共享變量而致使的數據一致性問題了。

以上,咱們介紹了 ThreadLocal 的兩種不一樣的使用場景,其中第二種更加的常見一點,下面咱們來看原理。

基本原理

ThreadLocal 在使用上仍是很簡單的,可是其內部實現以及與各個線程的關聯仍是有些繞的,接下來咱們深刻去看看。

基本字段屬性

image

除了 threadLocalHashCode 是一個常量,每當建立一個新的 ThreadLocal 實例的時候就會根據 nextHashCode 和 HASH_INCREMENT 去計算初始的賦值。

由於 nextHashCode 是靜態的,是類共享的,因此,每建立一個 ThreadLocal 實例,它的 threadLocalHashCode 是前一個實例的基礎上加固定常量 0x61c88647

這個值經換算是一個斐波那契數,每次增量該常量能夠分散 hash 值的分佈,減小後續在 map 中定位保存數據時產生衝突。

內部類 ThreadLocalMap

ThreadLocalMap 的內部實現是很相似 HashMap 的內部實現的,若是你分析過 HashMap,這一塊會容易理解不少,下面咱們看其中重要的幾個字段:

image

首先,Entry 這個類是 ThreadLocalMap 中定義的內部類,很簡單,保存了兩個主要內容,一個是 ThreadLocal 的局部變量,一個是 Object 類型的 value 值。

INITIAL_CAPACITY 指定了 table 的初始化容量,或者說是默認的數組初始化長度。

size 指定了 table 中實際有效的 Entry 數量。

threshold 是一個閾值的概念抽象,當 table 的 size 達到了這個閾值,就會觸發一個動態擴容動做,擴容 table。

因此,對於 ThreadLocal 的一個不太恰當的理解是,它只是一個封裝了 hashCode 的 key,這個 key 決定了咱們的 value 該保存在 ThreadLocalMap 內部 table 的哪一個位置。

這一點也在它的構造函數中也可見一斑:

image

這個 i 就是當前 Entry 要保存在 table 上的具體索引,它是如何計算的?

就是用咱們的 key(ThreadLocal 實例)內部保存的 hashcode 取餘 table 容量計算而來。

threshold 會被設置爲 table 容量的三分之二。

至於其中的 set、get 方法咱們待會分析,至此 ThreadLocal 中已經不剩下什麼重要的東西了,雖然 ThreadLocalMap 是 ThreadLocal 的內部類,可是與 ThreadLocal 所表現出來的語義並無很密切的關係,可能爲了某些安全性吧,將 ThreadLocalMap 定義爲了 ThreadLocal 的靜態內部類。

set、get方法原理

介紹以前,咱們先看 Thread 類中的一個字段:

image

Thread 類中持有了兩個 ThreadLocalMap 實例,兩個實例稍有區別,inheritableThreadLocals 相比於 threadLocals 來講具備更大的特殊性。

區別在於,若是父線程(即建立本身的那個線程)使用了 inheritableThreadLocals 存儲線程本地變量,那麼本線程的建立過程當中也會使用 inheritableThreadLocals 進行本地變量的存儲而且將父線程中全部的本地變量進行一份拷貝,填充到本身的 inheritableThreadLocals 中。

具體怎麼實現的你們能夠自行去查看,jdk 中從新定義了一個 InheritableThreadLocal 類,繼承的 ThreadLocal 並重寫了其中的 getMap 方法,致使你外部的 get 操做會轉而返回 inheritableThreadLocals 而再也不是 threadLocals。

如今咱們來看 ThreadLocal 的 set 方法:

image

set 方法仍是很簡單的,獲取當前線程內部的 ThreadLocalMap 實例,若是不是空的就往裏面增長一條記錄,反之先初始化一個 map 再增長一條記錄進去。

核心仍是在 ThreadLocalMap 的 set 方法:

image

這個方法的大致邏輯以下:

  1. 根據 ThreadLocal 這個 key 計算出當前節點應該保存在 table 的哪一個索引位置
  2. 若是該位置上不是空,產生了 hash 衝突,被別的節點提早佔有了。那麼會將該節點保存在 i+1 的索引位置上
  3. 若是該位置是空,那麼將本身掛在這個位置上
  4. 最後,若是添加結束後,發現 table 中有效節點數達到了閾值 threshold,那麼將調用 rehash 方法進行一次擴容並轉移數據的過程。

可能有些細心的人會疑問,爲何整個方法內沒看到一行處理併發的同步語句?

有這樣的疑問,你可能尚未徹底理解 ThreadLocal 的設計思路,ThreadLocalMap 已是線程的私有領地了,別的線程是不可能訪問的到的,又何來同步問題?

get 方法:

image

既然存是用的 ThreadLocal 實例做爲 key,取天然也是根據該實例進行 get 了,並不難理解。

到這裏,關於 ThreadLocal 基本的類結構體系、與 Thread 的關聯關係,以及核心的 set、get 方法邏輯實現咱們都予以了分析,不知道你理解的怎樣了呢?歡迎你和我交流!

內存泄露

在這以前,咱們關注一個問題,不少人對 ThreadLocal 的一個誤解,以爲他是不安全的,會產生『內存泄漏』的問題,咱們一塊兒來看看是否是這樣。

首先,ThreadLocal 確實是存在『內存泄漏』這個內存隱患的,可是一大堆人把源頭指向 Entry 這個節點類。

image

很明顯,咱們 Entry 將 key 存儲爲『弱引用』,什麼是弱引用這裏再也不贅述了,而將 value 存儲爲『強引用』,因而他們的內存結構就是這樣的(盜了張圖):

image

咱們的 ThreadLocal 實例被建立在堆中,方法棧中存在一個對它的強引用,咱們的 Entry 實例中存在一個對他的弱引用。

重點來了,有人就認爲,一旦我在主程序中丟失了對該實例的強引用,或是賦空了該實例,那麼 GC 會無視該實例存在着一個弱引用,而直接回收了該資源,以致於你永遠沒法訪問到該 Entry 實例的 value 屬性且沒法回收它,因此致使的內存泄漏。

看起來是有道理,可是不使用弱引用就沒有內存泄漏了嗎?

你換成強引用,會致使整個 Entry 實例都是無用數據,更大的內存泄漏。反而使用弱引用後,當你調用 get 方法的時候,會因爲 key 爲 null,執行清除邏輯,將 Entry 實例賦 null,最後由 GC 回收該內存資源。

但這始終不能解決 ThreadLocal 的內存泄漏問題,建議的作法是,當某個本地變量不用的時候,手動的調用 remove 方法進行移除。期待 jdk 能更新 ThreadLocal 的實現,代碼層解決這個問題。

關注公衆不迷路,一個愛分享的程序員。

公衆號回覆「1024」加做者微信一塊兒探討學習!

每篇文章用到的全部案例代碼素材都會上傳我我的 github

https://github.com/SingleYam/overview_java

歡迎來踩!

YangAM 公衆號

相關文章
相關標籤/搜索