ThreadLocal不僅要應付面試,更要真的理解,真的會用

前言

記得我幾年前第一次面試的時候,就是被問了這個,記得面試官直接就讓我說說ThreadLocal的實現原理以及平時有沒有見過哪些地方用到了。
我當時初入職場,還是一個大菜鳥,所以直接就被幹蒙了,至今還記憶猶新。

閒來無事,總結一下這塊,其實仔細想想這個ThreadLocal,整體思路其實挺清晰的,但有些細節會有難度,可能會涉及到一些比較深的平時不用的知識,說實話我也還沒有完全理清楚,但一直都在努力中。

概念

定義

我們說的ThreadLocal是java.lang包下的一個類,這個類提供特殊的線程局部變量,使得每個訪問該變量的線程在其內部都有一個獨立的初始化變量副本。
用人話解釋:
先說普通類中定義的變量,我們都知道是多個線程共有的。
而ThreadLocal這個類中有個特殊的變量,特殊就特殊在針對不同的線程,在用這個ThreadLocal的時候,都能拿到本線程獨有的值,你可以set,可以get,線程之間互不影響。

其實ThreadLocal這個概念,並不是java語言獨有的,其實很多語言都有這個概念,只不過java中是用哈希表實現了這個概念。

特點

簡單,開銷小,線程安全。

哪裏用ThreadLocal

1、Quartz的SimpleSemaphore,提供資源隔離

在這裏插入圖片描述
在這裏插入圖片描述
看上圖:
SimpleSemaphore裏面就有個方法:obtrainLock方法,用synchronized鎖
這個方法中有個很重的while操作(消費者處理完所有事情,需要等待新的事情,這個等待是一個while循環)
lockName是這個方法的入參,這個while方法的判斷邏輯是如果locks這個HashSet中有這個lockName,這個線程就執行wait()方法,由於obtrainLock本身是一個所方法,然後再去執行wait(),你的線程就被完全阻塞在這裏排隊了。
試想,如果沒有ThreadLocal先過濾,那麼同一個線程的多次調用這個obtrainLock方法,帶着相同的lockName,就會多次進入這個while循環,其實同一個線程是不需要多次進入這個操作的
所以通過在這個加鎖操作之前用ThreadLocal判斷(isLockOwner方法),將同一個線程帶着相同lockName調用這個方法的次數,就減少到一次了,即只會第一次進入while循環,其他的都被isLockOwner方法擋住了
最終使得訪問後面很重的操作的頻率大大降低,算是一個優化。

2、Mybatis的SqlSessionManager,資源持有

我們知道Mybatis連數據庫後,會有個連接池,裏面會維護有多個連接,每次操作數據庫,都需要拿到連接,再去操作,拿連接就是那個sqlSession.getConnection方法,每次操作都可能拿到任何一個連接。
如果想要支持事務,那必須讓一次事務的所有操作,都必須讓同一個連接處理,這樣才能要麼一起成功,要麼一起失敗,而一次事務的每個操作都需要從線程池中拿連接,那如何保證一次事務的每次操作拿到的都是同一個連接呢?
一次事務的多個操作一般都是一個線程去執行的,那其實問題就變成如何保證一個線程拿到的總是相同的一個連接,這裏就用到了ThreadLocal,將當前線程拿到的連接保存在ThreadLocal中,下次該線程拿連接,就直接從ThreadLocal中拿這個連接,這樣就保證了同一個線程永遠拿到同一個連接,而其他線程拿哪個連接不受這個線程的影響。

我們看看具體的代碼實現:
先是定義ThreadLocal,存放的就是SqlSession,每一個連接對應一個SqlSession
在這裏插入圖片描述
然後開始將一個線程的SqlSession放入ThreadLocal中
在這裏插入圖片描述
真正用的時候,比如commit,rollback等方法,就都從ThreadLocal中獲取連接了。
在這裏插入圖片描述

3、Spring的TransactionContextHolder

在這裏插入圖片描述
TransactionContext也叫分佈式事務資源池,保存的是當前環境的上下文,裏面有個PlatformTransactionManager,這個就是執行commit和rollback的類,所以在分佈式事務中也要保住同一個線程用同一個PlatformTransactionManager去執行commit或rollback,所以最終TransactionContext用ThreadLocal保存起來,達到效果。

4、登錄

登錄的時候,可以把每個線程的登錄信息放在ThreadLocal中,就保證了同一個人的操作始終在同一個線程中。

ThreadLocal核心源碼解讀

1、首先,每個Thread中,都有一個成員變量threadLocals

這個是專門爲ThreadLocal加的,具體threadLocals的賦值過程,是在ThreadLocal中
threadLocals的類型是ThreadLocal.ThreadLocalMap,這個ThreadLocalMap是ThreadLocal中的自定義的一個內部map類,key是ThreadLocal對象,value是每個線程的那個獨有的變量副本。
在這裏插入圖片描述

2、ThreadLocal的get方法

在這裏插入圖片描述
先拿到當前線程
getMap方法,就是從當前線程中拿ThreadLocalMap,這個就是Thread中那個成員變量。
ThreadLocalMap的key是當前這個ThreadLocal對象,value就是我們這個get方法真正要返回的值。
如果能拿到ThreadLocalMap,那麼就返回ThreadLocalMap中當前ThreadLocal對象對應的value值。
如果拿不到ThreadLocalMap,就去初始化value,最後再返回value。

總結

我們看到ThreadLocal的實現,就能清楚的知道爲什麼ThreadLocal可以保存不同線程的不同值了。
是因爲其實最終這些值還是保存在了各個線程中的一個map中,而ThreadLocal僅僅是作爲這個map的一個key。
那麼對於一個線程,如果他遇到多個ThreadLocal,其實線程中的那個map就有多對值了。
有沒有一種反向操作的感覺,乍一看以爲這些值都是保存在ThreadLocal中的,最終發現還是在線程中保存。

注意

要注意的是,每個線程中的ThreadLocalMap是ThreadLocal中定義的一個靜態類,相當於ThreadLocal重寫了一個map,那有人會問了,爲什麼不直接用HashMap呢?

其實這是一個涉及到java垃圾回收的問題,重寫的這個ThreadLocalMap,主要就是爲了這個事情搞的。

我們知道其實HashMap中真正的數據是在一個個Entry中的,其實ThreadLocalMap也是這樣,只不過ThreadLocalMap中的Entry是繼承了WeakReference這個類。我們知道ThreadLocalMap中的key值,其實是ThreadLocal對象,在set某個對象的時候,需要根據這個對象的hash值去hash表中找槽,如果找到對應的槽後,槽上原來的對象被回收了,那對於的hash表上的位置的值就是null,那麼ThreadLocalMap就會對這種已經廢棄掉的null值對應的槽做一些處理(主要是重新回收這些槽,並重新分配hash表大小等)。這樣相當於同步了垃圾回收的結果。

這就是爲什麼要重寫hashMap了,因爲hashMap不會處理這些邏輯,不處理就會造成槽不斷的被已經回收的ThreadLocal的空對象佔用着釋放不出來,最後影響hash的查找,因爲時間久了,每次正常hash後應該放的槽都被null佔了,只能繼續向後移着放。

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