一、什麼是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
是有返回值的,不會被回收。