ThreadLocal總結一:問題及其理解彙總

背景

  1. 整理ThreadLocal重要問題及其相關理解。

過程

  • 如何分析內存泄漏

    1. 首先,ThreadLocal作者認爲如果ThreadLocal實例不在被線程使用了的話,那麼ThreadLocal實例就沒有存在的必要了。一旦線程不使用了,那麼下次GC的時候就會把ThreadLocal移除掉。因此把ThreadLocal設計成弱引用。
    2. 其次,在當前線程執行的一段時間內,如果大量積累值 v , 那麼就會出現內存泄漏。因此,內存泄漏研究的是哪個?是值 v 。
    3. 然後,理解GC的邏輯是,不管內存是否充足,如果ThreadLocal不在被線程使用了,那麼下次GC的時候,就會被清除掉。
    4. 最後,需要深刻理解,如果線程退出了或者說線程執行完成了,這些相關實例會被回收的,不會導致內存泄漏。而事實卻不是這樣的,因爲線程執行有一個時間段的,就是在一個連續的時間段內,我們對ThreadLocal進行了計算和處理,但是線程後續邏輯卻不需要再使用ThreadLocal實例了,那麼這個時候,我們有必要把容器中entry[]的key和value都置null,這樣對堆上的對象沒有了使用,下次GC的時候就會回收。
    5. 注意:其實,ThreadLocal設計成弱引用,這也是一種保護機制,因爲至少可以減少內存空間的佔用,如果把減少內存空間的佔用理解爲被及時GC掉了,那麼也可以理解爲,這樣的設計也是防止內存泄漏。只是作用不是那麼明顯,因爲ThreadLocal在堆上僅僅只有一份。但是,v也是隻有一份嗎?不是的,每一次更新,可是新創建的對象,因此v又很多對象。雖然是很多對象,但是有用了,僅僅只有一個,其他都會被GC掉。那也只有一份,爲什麼會內存泄漏呢?是因爲在線程執行的時間段內,有大量的線程,大量的線程就有大量的v,而大量的v並不像弱引用的ThreadLocal會被及時GC掉的。
  • 爲什麼容器被設計成Entry[]?

    1. 首先,考慮實際情況對ThreadLocal的使用,很少遇見使用了多個ThreadLocal實例的情況,如果一定需要使用多個ThreaddLocal實例的時候,我們完全可以定義一個引用對象,讓這個對象持有相應的變量即可。
    2. 然後,設計成Entry[],從表面上看,那麼就是能夠裝不同的ThreadLocal實例。
    3. 最後,猜測,作者對這個ThreadLocal有更加深層次的考慮。希望這樣的設計能夠滿足各種各樣使用場景。
  • 同一個線程中使用多個ThreadLocal實例發生了什麼

    1. 首先,雖然每個ThreadLocal實例都是new出來的,他們都是彼此獨立不一樣的。但是,他們卻共用了一個原子類,這個原子類記錄了,當前線程,取到原子類中數值是多少。
    2. 那他們是怎麼做到呢?就是當第一次new的時候,會加載類,連接,初始化。其中,初始化的時候,就給ThreadLocal實例的屬性threadLocalHashCode設置值並返回一個值。同一個線程,執行第二次new的時候,不需要加載類了,它會執行初始化操作,這個時候取得上次設置好了的值,然後自己在設置一個新值,供下一個new ThreadLocal來取值即可。
  • ThreadLocal爲什麼設計成弱引用?

    1. 原因很簡單,那就是,如果當前線程不再使用了ThreadLocal實例了,那麼這個ThreadLocal實例,就沒有存在的必要的了,無論內存是否充足,都希望下次GC的時候,能夠清除掉它。
    2. 設計成弱引用好處就是,能夠及時被GC掉,這樣可以釋放資源。
  • ThreadLocal是如何解決hash衝突的

    1. 通過線性探測方式,線性地找到下一個腳標中key爲null的情況。以此解決hash碰撞。爲什麼叫線性探測法呢?線性是說,下腳標依次取值,爲什麼叫探測呢?因爲它也不知道下一個腳標是否有值還是沒有值,有還是沒有值,嘗試一下就知道了。因此叫做:線性探測法。
    2. 又因爲這個ThreadLocal的屬性threadLocalHashCode & (length - 1)這個值求出來是不規則的,因此,需要循環檢測。這裏的循環檢測,就體現了這個環形數組。因爲循環到結尾的時候,又會回到最開始的地方。
  • ThreadLocal爲什麼要設計環形數組?

    1. 因爲環形數組可以節約內存空間,重複利用已經分配好的空間。這裏的本質原因是希望減少擴容的次數。因爲每次擴容,都需要分配原來的2倍空間,把舊的entry[]中的內容完全拷貝到新的entry[],需要一些計算過程,會耗性能。如果能夠減少擴容的頻次,那麼性能就會很高。這裏當然是考慮平均性能,因爲發生擴容的概念是小的。

    2. 如果沒有環形數組,擴容的頻次一定會增加的,而且內存佔用會更多。

    3. 如果使用了環形數組,不但節約了內存空間,還減少了擴容的次數。

  • 環形數組會增大hash碰撞的概率嗎?
    是。

    假如我們的GC都是非常及時的,而我們的魔數相關算法又是循環的。所以一定會產生碰撞的,而我們的存儲空間又是環狀的,魔數和環形數組一起使用確實會增大hash碰撞的概率的。這是一種推測,在科學領域,只要存在一個可能的反面,那麼這樣的推測就是合理的。

    當然,也有可能我們GC不及時,那麼一些無用的entry無法被及時清除掉,而又佔用了table的空間,這個時候,就可能會發生擴容,而擴容一旦發生,那麼發生的hash碰撞的概率就會減少。也就是length越長髮生的概率就會越低。
    作者想節約內存空間和減少擴容頻次,於是設計了環形數組。這個環形數組帶來好處,卻加大了hash碰撞的概率。大師之所以是大師,是因爲他有權衡的智慧。他認爲節約空間並且減少擴容頻次帶來的性能提升和資源利用的好處是大於增加hash碰撞概率缺點的。因爲,無論怎樣,作者的代碼設計一定是需要解決hash碰撞的。也就是說,作者無論是否使用環形數組,都需要寫解決hash碰撞的邏輯。

  • 用魔數0x61c88647來解決hash碰撞,可行嗎?

    使用算法threadLocalHashCode & (length - 1)。完全可行的。因爲從做的測試來說,是均勻完美散列的。完美散列是指16長度,然後產生0-15中的每個數字。

    確實可以滿足解決hash碰撞。但是,作者把ThreadLocal設計成了弱引用。在實際使用過程中,可能存在有的ThreadLocal已經完成了職能,被GC掉了,這個時候,entry[]中的某個具體的entry就不在佔用空間了。而我們知道魔數的特性是循環的,那麼一定會產生hash碰撞的。

    因此,作者這樣的實現是一定會存在hash碰撞的。

  • threadLocalHashCode & (length - 1)求得的值是不規則的?

    1. 首先,求得的值是完美散列且是均勻的。
    2. 其次,後面在執行set操作的時候,會執行cleanSomeSlots()函數,這個函數是試探性地去除無效的entry實例,並不能保證一定能夠把無效的entry移除掉。它循環判斷的次數是以2爲低,長度爲指數的log函數。因此,方法名some,表達了可能是0個,可能是1個,也可能是多個。
    3. 最後,因爲這樣的散列是均勻而完美的。後續的log函數次數的遍歷才更加合理。但是,有的會認爲,爲什麼不直接遍歷數組呢?沒有必要,因爲遍歷整個數組,會耗費性能的。這樣每次執行set操作的時候,都需要遍歷一次數組。因爲能夠及時被GC掉的ThreadLocal,也不是頻繁發生的。完全沒有必要遍歷一次數組。因爲,遍歷一次不一定能夠找到,爲什麼不少執行幾次,試探性刪除呢?因爲這裏的刪除並不是必須的。

小結

  1. 如果能夠靜下心來,把基本知識、工作過程、源碼細節通讀後,一定會理解背後爲什麼這樣設計。
  2. 在這個過程中,需要提出很多問題、需要去找資料、需要寫測試代碼、需要分析、需要讀源碼、需要去想、需要總結纔可以最終理解。
  3. 還有很多其他問題,ThreadLocal在不同線程間訪問呢?父線程訪問子線程的值呢?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章