一、什麼是ThreadLocal
ThreadLocal用於儲存執行緒全域性變數,以方便呼叫。即,當前執行緒獨有,不與其他執行緒共享;可在當前執行緒任何地方獲取到該變數。
二、ThreadLocal的使用
1、如何儲存內容
創ThreadLocal
例項,並呼叫set
函式,儲存中國
字串,分別在當前執行緒和new-thread
執行緒獲取該值。通過列印結果可以看到,雖然引用的是同個物件,但new-thread
執行緒獲取到的值卻是null
。
執行結果:
main 中國
MainActivity: new-thread null
複製程式碼
這是什麼情況呢?
在ThreadLocal
的set
函式中,獲取當前執行緒的ThreadLocalMap
例項,如何當前執行緒第一次使用ThreadLocal
,則需要建立ThreadLocalMap
例項,否則直接通過ThreadLocalMap
例項的set
函式進行儲存。
2、如何獲取內容
由於main
執行緒前面set
函式將內容儲存到ThreadLocalMap
例項中,已經可以獲取到中國
字串。而在new-thread
執行緒中,由於是第一次使用ThreadLocalMap
,所以此時map
是null
,並呼叫setInitialValue
函式。
在setInitialValue
函式中,呼叫了initialValue
函式,該函式直接返回了null
,這就是為什麼在new-thread
執行緒獲取的值是null
。因此setInitialValue
函式主要為當前執行緒建立ThreadLocalMap
物件。
3、ThreadLocalMap
ThreadLocalMap
內部持有一個數組table
,用於儲存Entry
元素。Entry
繼承至WeakReference
,並以ThreadLcoal
例項作為key
,和儲存內容 T作為value
。當發生GC時,key
就會被回收,從而導致該Entry過期。
每一個執行緒都持有一個ThreadLocalMap
區域性變數threadLocas
,如下圖所示。
3.1 ThreadLocalMap的建立
ThreadLocalMap物件的建立,也就是ThreadLocal 物件呼叫了自身的createMap
函式。
ThreadLocalMap的建構函式,建立了一個儲存Entry物件的table陣列,預設大小16。並通過threadLocal
的threadLocalHashCode
屬性計算出Entry在陣列的小標,進行儲存,並計算出閾值INITIAL_CAPACITY
的2/3。
threadLocalHashCode
屬性在ThreaLocal物件建立時會自動計算得出.
threadLocalHashCode
作為ThreadLocal的唯一例項變數,在不同的例項中是不同的,通過nextHashCode.getAndAdd
已經定義了下一個ThreadLcoal的例項的threadLocalHashCode
值,而第一個ThreadLocal的threadLocalHashCode
值則是從0開始,與下一個threadLocalHashCode
間隔HASH_INCREMENT
。
通過threadLocalHashCode & (len-1)
計算出來的陣列下標,分發很均勻,減少衝突。但是呢,衝突時還是會出現,如果發生衝突,則將新增的Entry放到後側entry=null
的地方。
三、原始碼分析
1、ThreadLocalMap的set函式
在上一節中,分析了ThreadLocal
例項的set
函式,最終是呼叫了ThreadLocalMap
例項的set
函式進行儲存。
通過程式碼分析可知,ThreadLocalMap
的set
函式主要分為三個主要步驟:
-
計算出當前
ThreadLocal
在table
陣列的位置,然後向後遍歷,直到遍歷到的Entry
為null
則停止,遍歷到Entry
的key
與當前threadLocal
例項的相等,直接更替value; -
如果遍歷到
Entry
已過期(Entry
的key
為null
),則呼叫replaceStaleEntry
函式進行替換。 -
在遍歷結束後,未出現1和2兩種情況,則直接建立新的
Entry
,儲存到陣列最後側沒有Entry的位置。
在第2步驟和最後都會清理過期的Entry
,這個稍後分析,先看看第2步驟,在檢測到過期的Entry,會呼叫replaceStaleEntry
函式進行替換。
replaceStaleEntry
函式,主要分為兩次遍歷,以當前過期的Entry為分割線,一次向前遍歷,一次向後遍歷。
在向前遍歷過程,如果發現有過期的Entry
,則保留其位置slotToExpunge
,直到有Entry
為null
為止。這裡只是判斷staleSlot
前方是否有過期的Entry
,然後方便後面進行清理。
在向後遍歷過程,如果發現有key
相同的Entry
,直接與staleSlot
位置的Entry
交換value
(上圖註釋有問題)。如果沒有碰到相同的key
,則建立新的Entry
儲存到staleSlot
位置。與此同時,如果向前遍歷沒有發現過期Entry,而在向後遍歷發現過期的ntry
,則需要更新過期位置slotToExpunge
,因為後面的清除內容是需要slotToExpunge
。
2、ThreadLocalMap清除過期Entry
在上一小節中,會通過expungeStaleEntry
函式和cleanSomeSlots
函式清理過期的Entry,它們又是如何實現呢?
expungeStaleEntry
函式清理過期Entry
過程被稱為:探測式清理。函式傳遞進來的引數是過期的Entry
的位置,工作過程是先將該位置置為null
,然後遍歷陣列後側所有位置的Entry,如果遍歷到有Entry
過期,則直接置null
,否則將它移到合適的位置:hash
計算出來的位置或離該hash
位置最近的位置。
經過這麼一次經歷,staleSlot
位置到後側最近entry=null
的位置就不存在過期的entry
,而每個entry
要麼在原有hash
位置,要麼離原有hash
位置最近。
expungeStaleEntry
函式的工作範圍:
expungeStaleEntry
函式一開始會將起點,即陣列第3的位置設定為null
。然後開始遍歷陣列後側元素,4和5位置無論是否在它的hash
位置,在這裡都保持不變。遍歷到第6時,發現entry
已過期,將第6設定為null
。此時3和6位置變成白色了。
A、遍歷到第7的時候,假設h != i
成立,那麼第7位置的entry
將被移到第6位置,空出第7位置。
B、接著遍歷到第8位置,假設h != i
不成立,則第8的entry
的位置不變。
接著繼續遍歷後側元素,重複著A和B步驟,直到碰到entry為null,退出遍歷。例如這裡的第10位置,entry=null。
由於探測性清理,碰到entry=null
的情況就會結束。而通過cleanSomeSlots
函式進行啟發式清理,碰到entry=null
不停止,而是由控制條件n決定,而在這個過程中,碰到過期entry
,n又恢復到陣列長度,加大清理範圍。
在啟發式清理過程,如果碰到過期Entry
,會導致控制條件n
恢復到陣列長度len
,從而導致迴圈次數增加,則往後nextIndex
次數增加,從而增加清理範圍。這種方式也不一定能完整清理後面所有過期元素,例如在控制n
右移所有過程中,沒有碰到過期的entry
,就結束了。
3、ThreadLocalMap的擴容機制
在第1節,呼叫ThreadLocalMap
的set
函式最後,會呼叫reHash
函式進行擴容。
在外層進行啟發式清理後,如果size>threshold
則會進行rehash,而在rehash
中,會清理整個陣列的過期Entry
,如果清理後,陣列長度還大於3/4*threshod
,則進行擴容resize
。
resize
函式直接建立新的陣列,長度為舊陣列的兩倍。然後重新計算舊陣列元素在新陣列的位置,複製。
四、記憶體洩露
正常情況下,用完ThreadLocal例項,將其置為null,在發生GC時,ThreadLocal物件就會被回收。但是此時如果執行緒還存活(例如執行緒池執行緒的複用),就會導致Entry的value物件得不到釋放,會造成記憶體洩露。所以,在使用完ThreadLocal例項後,呼叫remove
函式清除一下。
疑惑
發生GC的時候,Key會被回收麼,還能獲取到值麼?
正常情況下,如果ThreadLocal例項同時被強引用,所以在發生GC的時候,是不會回收的,也就是此時WeakReference.get
是有返回值的,不會被回收。