併發原理抽絲剝繭,線程本地變量ThreadLocal的實現原理


關於ThreadLocal

ThreadLocal咱們常常稱之爲線程本地變量,經過它可以實現線程與變量之間的綁定,也就是說每一個線程只能讀寫本線程對應的變量。對於同一個ThreadLocal對象,每一個線程對該對象讀寫時只能看到屬於本身的變量,這樣來看ThreadLocal也是一種線程安全的模式。ThreadLocal的功能以下圖所示,一個ThreadLocal對象就是一個線程本地變量,該變量能夠保存多個變量值,好比線程一對應變量值一,其它兩個線程也有本身的變量值。算法

ThreadLocal變量綁定

ThreadLocal例子

咱們經過一個小例子來了解ThreadLocal的使用方法。首先建立一個ThreadLocal對象,因爲是泛型因此須要指定保存的數據類型,這裏保存的是String類型。而後啓動五個線程,每一個線程都經過ThreadLocal對象的set方法設置要綁定該線程的變量值,要保存什麼值就傳入什麼值,而當咱們要使用時則調用ThreadLocal對象的get方法,該方法無需傳入參數值。最終的輸出結果以下。數組

Thread-1--->Thread-1的變量Thread-0--->Thread-0的變量Thread-4--->Thread-4的變量Thread-3--->Thread-3的變量Thread-2--->Thread-2的變量複製代碼
ThreadLocal例子

這個例子的效果以下圖,五個線程都各自有各自對應的變量。安全

ThreadLocal三個主要方法

  1. set方法,用於設置當前線程本地變量的值,傳入的參數爲要設置的值。好比 threadLocal.set("value") 。bash

  2. get方法,用於獲取當前線程本地變量的值,無需傳入任何參數。好比 String threadLocalValue = (String) threadLocal.get() 。數據結構

  3. remove方法,用於刪除當前線程本地變量,無需傳入任何參數。好比 threadLocal.remove() 。多線程

如何模擬實現

在瞭解了ThreadLocal的功能後咱們試着想一個問題:ThreadLocal是如何實現的呢,變量與線程之間如何綁定的呢?實際上,若是讓咱們本身來實現ThreadLocal功能,咱們只要經過一個Map結構就能實現該功能了。其中Map的key是當前線程,而Map的value則是變量值。下圖展現了ThreadLocal的設計思想。併發

模擬實現

再看具體的模擬實現代碼,該模擬類提供了set、get和remove三個方法,這三個方法都是間接操做Map對象。注意Map對象的key值都是當前線程,由Thread.currentThread()來獲取,這個key值沒必要由調用方傳入。這樣就實現了一個簡單的ThreadLocal,是否是很簡單?機器學習

模擬實現

JDK中ThreadLocal的實現思想

上面的實現方式雖然簡單且符合咱們的思考方式,可是它存在多線程併發性能問題,這個怎麼說呢?其實很明顯,咱們實現的ThreadLocal內部使用了一個Map對象,全部線程的操做都是針對該Map對象進行的操做,須要保證該對象訪問的線程安全,這就須要額外的鎖機制來保證,但與此同時也就帶來了性能問題。分佈式

JDK爲咱們提供的ThreadLocal的實現則比較巧妙,爲了不併發時涉及鎖問題,它在每一個線程對象中都放一個Map對象,但它並無直接使用JDK的Map類,而是本身實現了一個key-value數據結構。每一個線程都操做本身的Map對象則不存在併發問題,以下圖,線程一包含了一個Map對象,該Map對象的key是ThreadLocal對象,而value則是變量值。注意這裏的實現須要將思惟轉換一下,ThreadLocal對象變成了key,也就是說可能存在不少不一樣的ThreadLocal對象,要查找時須要傳入對應的ThreadLocal對象。源碼分析

ThreadLocal實現思想

JDK的實現源碼分析

注意這裏只分析實現的核心內容,並不是包括全部源碼細節,而且爲了達到簡潔清晰的效果,可能會刪除或修改少許源碼。咱們先來看Thread類與ThreadLocal類的關係,看到Thread類中包含了一個threadLocals變量,它是一種ThreadLocal.ThreadLocalMap類型,該類型定義在ThreadLocal類裏面,也就是一個內部類。而ThreadLocalMap這個內部類便是實現了一個Map結構,該類又包含了Entry內部類,ThreadLocal對象和變量值則是經過Entry來保存。

類關係

Thread類裏面聲明瞭threadLocals變量用於關聯ThreadLocal.ThreadLocalMap對象,注意默認爲null。

Thread類

而ThreadLocal類的大致結構以下,提供了主要的三個方法,其ThreadLocalMap內部類實現Map結構。Map結構具體由Entry類實現,該類繼承了WeakReference類,目的是爲了不內存泄漏。下面將對三個主要方法進行分析。

ThreadLocal類

對於多個線程與多個線程本地變量來講,它們的結構以下圖。

關於ThreadLocalMap類

ThreadLocalMap類實際上就是一個Map結構的實現,對於Java開發人員來講對Map再熟悉不過了,並且因爲ThreadLocalMap類的實現涉及到不少細節,若是咱們純講它繁瑣的實現源碼則會致使篇幅冗長,因此這裏咱們主要是瞭解它的結構和操做便可。ThreadLocalMap類使用數組來保存key-value,數組的每一個元素對應一個key-value,因此新增、修改、刪除等操做都是圍繞着數組進行的。保存以前會先用哈希算法計算線程對象的哈希值,這是一個整型值,經過該值就能定位數組的某個位置的元素,這樣就能找到對應的key-value進行操做。

ThreadLocal的set方法

咱們看set方法的實現,ThreadLocal類的set方法邏輯爲:首先獲取當前線程對象,而後經過getMap方法獲取當前線程的ThreadLocalMap,其實就是從Thread對象中獲取,最後調用ThreadLocalMap對象的set方法保存key-value。注意若是Thread對象中的ThreadLocalMap對象爲空的話則須要調用createMap方法先建立ThreadLocalMap對象並關聯到Thread對象中。

set方法

ThreadLocal的get方法

get方法的邏輯爲:首先獲取當前線程對象,而後經過getMap方法獲取當前線程的ThreadLocalMap對象,若是該對象不爲空則調用ThreadLocalMap對象的getEntry方法獲取Entry,Entry對象即包含了咱們要的value。若是獲取不到值則最終還會執行setInitialValue方法,它是根據ThreadLocal對象的initialValue方法來設置初始值,默認是null,若是你想要設置一個初始值則能夠重寫initialValue方法。

get方法

ThreadLocal的remove方法

remove方法的邏輯很簡單,直接獲取當前線程對象的ThreadLocalMap對象,而後調用該對象的remove方法刪除對應的key-value。

remove方法

ThreadLocal的內存泄漏

JDK的實現是讓Entry繼承了WeakReference類,因此能夠指定對某個對象進行弱引用,弱引用類型在沒有其它強引用的狀況下會被JVM的垃圾回收器回收。咱們經過下圖來理解如何致使內存泄漏,咱們知道ThreadLocal被建立後就會伴隨Thread的整個生命週期,假如這個線程的生命週期很長則會致使嚴重的內存泄漏,下面看具體的狀況。

運行棧運行過程當中假如某個時刻ThreadLocal引用再也不指向ThreadLocal對象,則該對象僅僅剩下一個弱引用,這時該對象就會被JVM回收,從而致使Entry的key爲null,key爲null時就致使ThreadLocalMap沒法再找到這個Entry的value。一旦運行時間被拉長,value將一直存在內存中而沒法被回收,這樣就形成了內存泄漏,整個引用關係爲Thread對象->ThreadLocalMap對象->Entry對象->value。

那是否是不要繼承WeakReference類,讓它默認強引用就不會致使內存泄漏呢?那確定不是,否則也就不用畫蛇添足了。運行棧運行過程當中假如某個時刻ThreadLocal引用再也不指向ThreadLocal對象,則ThreadLocal對象由於存在強引用而不被JVM回收,此時除了value沒法被回收外,ThreadLocal對象也沒法被回收,一樣產生內存泄漏問題。

綜上所述,無論Entry有沒有繼承WeakReference類都存在內存泄漏問題,若是咱們不手動去執行remove操做的話都會致使內存泄漏。那麼JDK團隊爲何又要繼承WeakReference類呢?那是由於他們想採起一些措施來儘可能保證內存不泄漏,也就是說他們會在ThreadLocalMap類的get、set、remove方法中去執行一個清除操做,把ThreadLocalMap包含的全部Entry中key爲null的value給清除掉,而且將對應的Entry也置爲null,以便被JVM回收。

因此咱們在使用ThreadLocal時要注意的一點是:當咱們使用完ThreadLocal時都要手動調用remove方法,從而避免內存泄漏。

總結

本篇文章介紹了ThreadLocal的相關知識,從簡單的使用例子開始一步一步深刻,並且咱們還本身模擬實現了一個ThreadLocal類,模擬的方式簡潔且容易理解,但卻存在併發性能問題,因此JDK實現的ThreadLocal相對複雜不少。而後咱們分析了JDK的ThreadLocal的實現思想,最後從源碼級別分析它的實現,包括set、get和remove三個主要方法。最後,咱們講解了ThreadLocal存在的內存泄漏問題,並提出了使用ThreadLocal的注意點是要手動調用remove方法清理掉再也不使用的key-value。

更多Java併發原理剖析可關注個人專欄。

專一於人工智能、讀書與感想、聊聊數學、計算機科學、分佈式、機器學習、深度學習、天然語言處理、算法與數據結構、Java深度、Tomcat內核等。

相關文章
相關標籤/搜索