併發編程之——ThreadLocal的作用與實現原理

前言

最近項目出了個問題,涉及到ThreadLocal,所以抽時間把這個知識點理一下,以一種更容易理解的方式。不過仔細研究才發現這玩意涉及到的東西真不少,本篇只做概要講解。

 

ThreadLocal簡介

直接翻譯叫線程本地,但是ThreadLocal壓根就不是線程本地屬性,線程本地屬性叫threadLocals,threadLocals通過存儲鍵值對的方式存儲線程私有數據(通常是業務對象),它的類型是ThreadLocal.ThreadLocalMap內部類,而ThreadLocal對象則是ThreadLocalMap的Key,它實際上是用於管理線程私有數據對象的Map,具體什麼樣呢?

public class XXXContext {
    // 1
    public static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>();

    // 2
    public static ThreadLocal<Session> sessionHolder= new ThreadLocal<Session>();

}

來看一張圖: 

可以看到,屬性threadLocals是存在線程私有內存上的,而Key的值則是靜態的ThreadLocal對象。這樣的結構往下將能單獨開一篇文章,先按下不表。

 

ThreadLocal的作用

無需講的冠冕堂皇,最大的用處是:線程隨時需要用一個對象去做一些事情,這個對象又不是公共的,而是線程私有的,線程又不想哪哪都帶着它,只是想什麼時候用到,隨手就可以招過來。

這樣的解釋你一定看得懂,但是這隻算是表象,標準完整的解釋是:Java提供ThreadLocal用於解決這樣一個問題,複雜業務流程中的某些數據對象在整個業務流程中處處都有使用,爲了降低參數傳遞的複雜性(說白了就是不想把這樣的數據對象作爲參數進行傳遞調用),線程提供了私有屬性threadLocals(ThreadLocal.ThreadLocalMap)以鍵值對的形式存儲這樣的數據,但是因爲這樣的數據可能導致內存泄露,因而提供了ThreadLocal類來協調處理這些線程私有數據。

不知道這樣的解釋你能否看懂,反正我說的夠累的。

那麼都有哪些對象符合上面的描述呢? 首先想到的就是數據庫連接,一個複雜業務,可能有很多次數據庫交互,R&D寫代碼如果哪哪都帶着這個連接,那代碼就顯得很不簡練,因而就需要把數據庫連接放到線程私有屬性上去,數據結構是一個鍵值對,其中Key是ThreadLocal對象,Value就是數據庫連接或者連接的包裝類;

 

ThreadLocal的實現方式

ThreaLocal有個靜態內部類ThreadLocalMap,它就是線程私有屬性的類型。

看下面代碼,ThreadLocalMap核心內容是:

  1. INITIAL_CAPACITY:初始化數組大小
  2. Entry[]:Entry對象數組
  3. size:數組當前數據量
  4. threshold:擴容閾值

是不是很熟悉,就是一簡化版的HashMap

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0

set邏輯

set邏輯很簡單,先取Key值得hashcode,與數組的length-1做與運算,得到落點index,如果這個落點已經被佔了,就往後移一位,如果當前落點已經是最後的空位且被佔用,則從0開始,直到找到空位爲止;找到完空位以後,就把Value放下去,放完以後還要檢驗數組的使用量是否超過了閾值,即:長度的2/3,如果超過了,還要對數組做擴容操作,擴容的規則同HashMap的數組擴容規則差不多,都是長度乘2,然後做rehash。——這段邏輯描述也是簡化版的,真實的邏輯還要複雜的多,而且有它必須複雜的原因。

——詳細源碼解析可以關注我的微信公衆號:源碼e站

ThreadLocal的缺點

很多人提到內存泄露,說實在的我覺得這個缺點不能安在ThreadLocal頭上,因爲這是線程自己的缺陷,而ThreadLocal只是在幫助線程儘量解決整個問題,這個需要單獨開篇分析。

 

1、上面分析set方法有可能出現落點被佔用的情況,這種情況因爲ThreadLocalMap不使用落點鏈表,所以它只能依次向後找空落點,當然這個不能說是缺點,因爲ThreadLocal絕大多數情況下達不到必須的數據規模;但是在get的時候則存在一定的缺點,那就是當落點不是當前key值,同樣需要依次向後找,最壞的情況下,get的時間複雜度是O(n*2/3),所以,ThreadLocal完全可以記錄Key值與最終落點的關係,這樣複雜度就可以降爲O(1)了。

2、最開始提到了,最近一個項目碰到了問題,這個問題就是ThreadLocal使用不當導致的,當然,這個也不能嚴格算是ThreadLocal的缺點。

什麼場景呢?就是ThreadLocal+數據庫連接池,問題出現的場景是,數據庫是多路由的,而連接池中的連接包含路由信息並且還會被複用,這樣一來,當一個連接被複用的時候,後面線程要操作的數據庫可能不是它想要的數據庫;

舉個例子,線程A使用連接1連接A庫,完事以後把連接1放回連接池,線程2過來了,複用連接1,這樣線程2也會去操作A庫,但實際上線程B要操作的是B庫,所以業務報錯,可能是找不到數據等等。

那解決的方法是:在使用ThreadLocal+連接池的時候,如果數據是線程共享的(比如上面例子中的路由信息),一定要在使用完主動調用remove方法,清除共享數據。

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