終於明白阿里百度這樣的bat,爲什麼喜歡面試拿ThreadLocal考驗面試者了

ThreadLocal 簡介

ThreadLocal 是一個解決多線程併發問題的工具類,ThreadLocal有的人可能理解爲本地線程,這個並不是正確的理解。ThreadLocal並不是一個線程,應該把它理解爲一個線程本地變量

它底層的實現原理是通過爲每一個線程提供一個共享變量的副本,每個線程的操作只是對自己線程內部的變量副本進行操作

這裏提到線程操作只對自己的內部變量副本進行操作很多人第一反應就會想到線程的工作內存以及JMM的知識點,會誤以爲ThreadLocal是解決併發中共享數據的訪問問題

但是,ThreadLocal並不是爲了解決多線程共享變量的問題,它只是爲每個線程提供一個變量副本,這個變量副本對於其它線程來說是不可見的

解決併發中共享數據的訪問問題和爲線程提供一個只能在自己線程內部操作的線程本地變量還是有本質區別的。

並且,ThreadLocal是一直伴隨着當前線程的整個生命週期,隨着線程的消亡而消亡,他這個變量副本不需要與其它線程進行共享。

在jdk1.2的時候就提供了ThreadLocal類,在jdk逐漸升級的過程也對ThreadLocal進行了升級優化,在jdk1.5的時候ThreadLocal開始支持泛型

ThreadLocal 源碼實現

從上面的簡介出發,我們說到ThreadLocal 本質上是一個線程本地變量,對於一個變量來說,要操作它就要對外提供get、set、remove、init等方法。

對於ThreadLocal的源碼底層的實現是比較簡單的,主要就是幾個的方法源碼,而且實現的邏輯也很少,所以強烈的推薦閱讀,對於初級開發人員來說難度也不大。

我們來看看ThreadLoca的底層源碼,在ThreadLoca中,主要的方法包含以下幾個:

  1. get:獲取當前線程本地變量的值。
  2. setInitialValue:第一次獲取當前線程本地變量的值的時候,進行值得初始化,若不重寫setInitialValue方法,初始值爲null。
  3. set:設置當前線程變量的值。
  4. remove:刪除當前線程本地變量的值。

接下來我們對這幾個方法一層一層進行剖析,先來看看get方法得源碼實現:

get的方法的源碼非常的簡單,從中可以看出主要分爲三步

  1. getMap獲取map的值。
  2. 從map中獲取value值。
  3. 若是上面兩步獲取值爲null,就進行初始化。

get源碼中有一個getMap的方法實現,我們來看看getMap方法的源碼實現:

這個方法實現非常的簡單t.threadLocals來獲取ThreadLocalMap對象,從這一點就可以證實ThreadLocalMap是屬於當前Thread的,如果Thread對象不同,那麼獲取的對象肯定是不一樣的

倘若獲取的值爲null,就會調用setInitialValue方法進行初始化,來看看他的初始化的源碼:


從初始化的源碼中可以看出value直接就是爲null,初始化map肯定是不存在的,所以調用createMap的方法進行創建map,來看看創建map的源碼

直接就是調用new ThreadLocalMap(this,firstValue)進行初始化,並賦值給當前線程的threadLocals引用,在每個線程中都有一個空的threadLocals引用,源碼如下:
在這裏插入圖片描述
所以這裏又再一次的證明了,ThreadLocal是屬於當前線程的,也就是ThreadLocal中的ThreadLocalMap屬於當前線程,對於其它線程是不可見的

從上面的源碼分析邏輯中,可以深入的瞭解到ThreadLocal的get方法處理邏輯,我畫了一個get方法的處理流程圖,如下圖所示:

分析完get方法的源碼來看看set方法的源碼,可以說get方法的源碼在ThreadLocal已經算是比較複雜的了,這回大家心裏是不是在想,這也太簡單了吧。

ThreadLocal就是那麼簡單,下面的幾個方法的源碼的分析就會越來越簡單,我們來看看set方法的源碼:

這幾個方法都很熟悉了呀,getMap的實現就是返回t.threadLocals,然後就是判斷map是否爲空,不爲空就map.set(this,map),爲空就調用createMap創建map值。

set方法非常的簡單。所以在第一次進行調用get和set方法的時候,都會進行初始化線程的ThreadLocalMap,但是如果先調用get方法,初始化的ThreadLocalMap的value值是null

看完get和set方法的源碼,最後就來看看remove方法的源碼:


在移除操作中是調用了ThreadLocalMap的remove方法,而ThreadLocalMap的remove方法中會根據key的threadLocalHashCode值和數組的長度先計算下標值。

然後循環的從i下標開始循環的獲取key數組中的Entry值e,若是Entry對象的key與要移除的key相等,纔會把它移除掉。

Entry對象是ThreadLocalMap中的一個static內部類,ThreadLocalMap內部是使用Entry來實現key-value來實現存儲的,並且Entry繼承弱引用:

這裏的Entry類似與我們的HashMap結構,都是使用key-value形式進行存儲,但是他們還是有區別的。

Map集合中的hash類結構解決hash衝突採用的是拉鍊法,而在Entry解決hash衝突採用的是開放定址法

什麼是開放定址法呢? 當發生衝突的時候,第一個key佔據了一個位置,第二個key第一個key的位置向下查找,找到下面的一個空位插入, 如果沒有繼續查找空位,直到找到爲止並進行插入。

這個可以從Entry的源碼中可以看出,我們來看看Entry裏面的set方法的源碼:

從Entry的源碼中就可以看出,當計算出下標i的值,並獲得對應數組i位置的值若是key不相等,從該位置一直循環獲取key值並於當前的key值進行比較,直到相等爲止。

ThreadLocal 存在問題

在set方法中,有一個重要的作用就是防止內存泄漏,在set中分別調用了replaceStaleEntry()cleanSomeSlots()方法,兩者的作用分別如下:

  1. replaceStaleEntry:當判斷到key爲null,但是存在值說明,之前的key(ThreadLocal對象)被清除掉,但是內存一直佔用着,就用新的元素替換掉舊的元素。
  2. cleanSomeSlots:則清除掉原來key == null的Entry。

在說ThreadLocal內存泄漏的解決的方案之前,先來說說造成內存泄漏的原因,這裏我畫了一張圖:

在Thread有一個ThreadLocalMap的引用,而ThreadLocalMap的key在源碼中可以看出是ThreadLocal的對象。

但是ThreadLocal是一個弱引用,在垃圾回收的時候會被回收掉。什麼是弱引用呢? 弱引用就是被弱引用關聯的對象只能存活到下一次垃圾收集發生之前。當垃圾收集器工作時,弱引用關聯的對象都會被回收。

但是ThreadLocalMap的生命週期和Thread是一樣的,它不會被回收掉,所以就會存在有引用指向Entry對象。

若是線程一直不結束,就會一直存在一條引用鏈:ThreadLocalRef->Thread->ThreadLocal->ThreadLocalMap->Entry->value導致ThreadLocalMap中的key沒有了,但是value還存在,這就造成了內存泄漏

此時若是key值的引用ThreadLocal對象被回收掉,就無法通過key獲取對象,就會存在內存中又無法使用。

在ThreadLocal裏面爲了防止內存泄漏的情況,在新增、移除、獲取的時候,都會去查出key==null的entry對象。

所以爲了避免內存的泄漏,每次使用完ThreadLocal時候都養成調用remove方法來擦出數據的習慣,即時的清理出乾淨的內存。

ThreadLocal 應用場景

看上面的介紹我們已經詳細的瞭解了ThreadLocal的作用和實現的原理,那麼它的應用場景都有哪些呢?

這裏主要列舉了兩方面,一個就是數據庫連接,Session會話管理;另一個就是在Spring的MVC三層架構方面,用來獲取request 這個參數。

我們先來說一說數據庫連接,Session會話管理這一方面的應用,有深入瞭解過數據庫的人可能知道,在使用我們Java代碼操作數據庫,首先要建立連接。

但是這裏就有一個問題,一個人使用完數據庫後,要經歷連接->斷開這樣的一個過程,加入大量的用戶過來,那麼這對數據庫來說是一筆巨大的開銷。

我們這裏就可以使用ThreadLocal,這樣ThreadLocal就會在每個線程中都都創建一個副本,並且這個副本在線程的任何地方都可以使用。這樣就不用頻繁的斷開又建立連接。

另一方面對request這個參數的管理,可能在多個很多方法中都會用到request這個參數。我們可以把這個參數都放到ThreadLocal中,隨時用就隨時取。

並且對於多線程中,request是不需要被共享的,只要屬於當前線程即可,所以說它並不是解決多線程變量的共享問題,這個和Synchronized 還是又明顯區別的。

說完ThreadLocal的場景的分析,下面來說一說常見的ThreadLocal的面試問題,這裏的面試問題的答案僅是個人的理解和思考,大家可以參考,若是又更加深入的思考可以在留言區留言。

ThreadLocal 面試問題


問點一:爲什麼使用線程的id來作爲ThreadLocalMap的key值呢?

這個很容易理解,不使用Thread的id來作爲ThreadLocalMap的key主要是爲了解決存儲多value的情況。

一個線程就對應一個ThreadId,假如有多個value需要存儲,就沒辦法存儲,而已ThreadLocal作爲key,每個ThreadLocal的唯一性使用threadLocalHashCode來區分,就可以對應多個value進行存儲。

問點二:線程之間如何傳遞ThreadLocal對象呢?

在ThreadLocal的子類InheritableThreadLocal中有了對應的實現,InheritableThreadLocal重寫了ThreadLocal 中的三個相關的方法。

通過這個實現,可以實現子父線程之間的數據傳遞,在子線程中能夠使用父線程的ThreadLocal本地變量。

問點三:ThreadLocal是如何做到變量在線程之間是互不干擾的?

在ThreadLocal中內部維護了一個ThreadLocalMap用於儲存數據,而每一個線程中都會有一個ThreadLocalMap的引用,在第一次使用ThreadLocal的時候就會創建map。


線程不同必然t.threadLocals是不同的,每個線程在第一次使用的時候,都會進行初始化map,因此在不同的線程中也就互不干擾,獨立於線程而存在。

問點四:ThreadLocal中造成內存泄漏的原因?解決方案是什麼?

因爲ThreadLocal被包裝成弱引用,在ThreadLocal對象被回收時,還會存在ThreadLocalRef->Thread->ThreadLocal->ThreadLocalMap->Entry->value引用鏈。

這樣就導致這塊內存無法被回收,且又不能被使用,造成了內存泄漏,爲了能夠有效的避免內存泄漏,就要養成使用完後調用remove方法擦出數據習慣。

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