上篇文章 深刻理解 Handler 消息機制 中提到了獲取線程的 Looper 是經過 ThreadLocal
來實現的:java
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
複製代碼
每一個線程都有本身的 Looper,它們之間不該該有任何交集,互不干擾,咱們把這種變量稱爲 線程局部變量 。而 ThreadLocal
的做用正是存儲線程局部變量,每一個線程中存儲的都是獨立存在的數據副本。若是你仍是不太理解,看一下下面這個簡單的例子:git
public static void main(String[] args) throws InterruptedException {
ThreadLocal<Boolean> threadLocal = new ThreadLocal<Boolean>();
threadLocal.set(true);
Thread t1 = new Thread(() -> {
threadLocal.set(false);
System.out.println(threadLocal.get());
});
Thread t2 = new Thread(() -> {
System.out.println(threadLocal.get());
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(threadLocal.get());
}
複製代碼
執行結果是:github
false
null
true
複製代碼
能夠看到,咱們在不一樣的線程中調用同一個 ThreadLocal 的 get() 方法,得到的值是不一樣的,看起來就像 ThreadLocal 爲每一個線程分別存儲了不一樣的值。那麼這究竟是如何實現的呢?一塊兒來看看源碼吧。算法
如下源碼基於 JDK 1.8 , 相關文件:數組
ThreadLocal.javabash
首先 ThreadLocal 是一個泛型類,public class ThreadLocal<T>
,支持存儲各類數據類型。它對外暴露的方法不多,基本就 get()
、set()
、remove()
這三個。下面依次來看一下。微信
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 獲取當前線程的 ThreadLocalMap
if (map != null)
map.set(this, value);
else
createMap(t, value); // 建立 ThreadLocalMap
}
複製代碼
這裏出現了一個新東西 ThreadLocalMap
,暫且就把他當作一個普通的 Map。從 map.set(this, value)
能夠看出來這個 map 的鍵是 ThreadLocal
對象,值是要存儲的 value
對象。其實看到這,ThreadLocal 的原理你應該基本都明白了。多線程
每個
Thread
都有一個ThreadLocalMap
,這個 Map 以ThreadLocal
對象爲鍵,以要保存的線程局部變量爲值。這樣就作到了爲每一個線程保存不一樣的副本。併發
首先經過 getMap()
函數獲取當前線程的 ThreadLocalMap :
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
複製代碼
原來 Thread 還有這麼一個變量 threadLocals
:
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. * * 存儲線程私有變量,由 ThreadLocal 進行管理 */
ThreadLocal.ThreadLocalMap threadLocals = null;
複製代碼
默認爲 null
,因此第一次調用時返回 null ,調用 createMap(t, value)
進行初始化:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
複製代碼
set()
方法是向 ThreadLocalMap
中插值,那麼 get()
就是在 ThreadLocalMap
中取值了。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 獲取當前線程的 ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result; // 找到值,直接返回
}
}
return setInitialValue(); // 設置初始值
}
複製代碼
首先獲取 ThreadLocalMap,在 Map 中尋找當前 ThreadLocal 對應的 value 值。若是 Map 爲空,或者沒有找到 value,則經過 setInitialValue()
函數設置初始值。
private T setInitialValue() {
T value = initialValue(); // 爲 null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
複製代碼
setInitialValue()
和 set()
邏輯基本一致,只不過 value 是 null
而已。這也解釋了文章開頭的例子會輸出 null。固然,在 ThreadLocal 的子類中,咱們能夠經過重寫 setInitialValue()
來提供其餘默認值。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
複製代碼
remove()
就更簡單了,根據鍵直接移除對應條目。
看到這裏,ThreadLocal
的原理好像就說完了,其實否則。ThreadLocalMap
是什麼樣的一個哈希表呢?它是如何解決哈希衝突的?它是如何添加,獲取和刪除元素的?可能會致使內存泄露嗎?
其實 ThreadLocalMap
纔是 ThreadLocal
的核心。ThreadLocal 僅僅只是提供給開發者的一個工具而已,就像 Handler 同樣。帶着上面的問題,來閱讀 ThreadLocalMap 的源碼,體會 JDK 工程師的鬼斧神工。
ThreadLocalMap 是 ThreadLocal 的靜態內部類,它沒有直接使用 HashMap,而是一個自定義的哈希表,使用數組實現,數組元素是 Entry
。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
複製代碼
Entry
類繼承了 WeakReference<ThreadLocal<?>>
,咱們能夠把它當作是一個鍵值對。鍵是當前的 ThreadLocal
對象,值是存儲的對象。注意 ThreadLocal 對象的引用是弱引用,值對象 value 的引用是強引用。ThreadLocal 使用弱引用其實很好理解,源碼註釋中也告訴了咱們答案:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys
Thread
持有 ThreadLocalMap
的強引用,ThreadLocalMap
中的 Entry
的鍵是 ThreadLocal
引用。若是線程長期存活或者使用了線程池,而 ThreadLocal
在外部又沒有任何強引用了,這種狀況下若是 ThreadLocalMap
的鍵仍然使用強引用 ThreadLocal
,就會致使 ThreadLocal 永遠沒法被垃圾回收,形成內存泄露。
那麼,使用弱引用是否是就萬無一失了呢?答案也是否認的。一樣是上面說到使用狀況,線程長期存活,因爲 Entry 的 key 使用了弱引用,當 ThreadLocal 不存在外部強引用時,能夠在 GC 中被回收。可是根據可達性分析算法,仍然存在着這麼一個引用鏈:
Current Thread -> ThreadLocalMap -> Entry -> value
key 已經被回收了,此時 key == null
。那麼,value
呢?若是線程長期存在,這個針對 value 的強引用也會一直存在,外部是否對 value 指向的對象還存在其餘強引用也不得而知。因此這裏仍是有概率發生內存泄漏的。就算咱們不知道外部的引用狀況,但至少在這裏應該是能夠切斷 value
引用的。
因此,爲了解決可能存在的內存泄露問題,咱們有必要對於這種 key 已經被 GC 的過時 Entry 進行處理,手動釋放 value 引用。固然,JDK 中已經爲咱們處理了,並且處理的十分巧妙。下面就來看看 ThreadLocalMap
的源碼。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
複製代碼
table
是存儲 Entry 的數組,初始容量 INITIAL_CAPACITY
是 16。
firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
是 ThreadLocalMap 計算哈希的方式。&(2^n-1)
其實等同於 % 2^n
,位運算效率更高。
threadLocalHashCode
是如何計算的呢?看下面的代碼:
private static final int HASH_INCREMENT = 0x61c88647;
private static AtomicInteger nextHashCode = new AtomicInteger();
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
複製代碼
0x61c88647
是一個增量,每次取哈希都要再加上這個數字。又是一個神奇的數字,讓我想到了 Integer
源碼中的 52429
這個數字,見 走進 JDK 之 Integer 。0x61c88647
背後確定也有它的數學原理,總之確定是爲了效率。
原理就不去探究了,其實我也不知道是啥原理。不過咱們能夠試用一下,看看效果如何。按照上面的方式來計算連續幾個元素的哈希值,也就是在 Entry 數組中的位置。代碼以下:
public class Test {
private static final int INITIAL_CAPACITY = 16;
private static final int HASH_INCREMENT = 0x61c88647;
private static AtomicInteger nextHashCode = new AtomicInteger();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static int hash() {
return nextHashCode() & (INITIAL_CAPACITY - 1);
}
public static void main(String[] args) {
for (int i = 0; i < 8; i++) {
System.out.println(hash());
}
}
}
複製代碼
運算結果以下:
0
7
14
5
12
3
10
1
複製代碼
計算結果分佈仍是比較均勻的。既然是哈希表,確定就會存在哈希衝突的狀況。那麼,ThreadLocalMap 是如何解決哈希衝突呢?很簡單,看一下 nextIndex()
方法。
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
複製代碼
在不超過 len
的狀況下直接加 1,不然置 0。其實這樣又能夠當作一個環形數組。
接下來看看 ThreadLocalMap 的數據是如何存儲的。
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); // 當前 key 的哈希,即在數組 table 中的位置
for (Entry e = tab[i];
e != null; // 循環直到碰到空 Entry
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) { // 更新 key 對應的值
e.value = value;
return;
}
if (k == null) { // 替代過時 entry
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
複製代碼
經過 key.threadLocalHashCode & (len-1)
計算出初始的哈希值
不斷調用 nextIndex()
直到找到空 Entry
在第二步遍歷過程當中的每一個元素,要處理兩種狀況:
(1). k == key
,說明當前 key 已存在,直接更新值便可,直接返回
(2). k == null
, 注意這裏的前置條件是 entry != null
。說明遇到過時 Entry,直接替換
不屬於 3 中的兩種狀況,則將參數中的鍵值對插入空 Entry 處
cleanSomeSlots()/rehash()
先來看看第三步中的第二種特殊狀況。Entry
不爲空,但其中的 key
爲空,何時會發生這種狀況呢?對,就是前面說到內存泄漏時提到的 過時 Entry。咱們都知道 Entry 的 key 是弱引用的 ThreadLocal
,當外部沒有它的任何強引用時,下次 GC 時就會將其回收。因此這時候的 Entry 理論上也是無效的了。
因爲這裏是在 set() 方法插入元素的過程當中發現了過時 Entry,因此只要將要插入的 Entry 直接替換這個 key==null
的 Entry 就能夠了,這就是 replaceStaleEntry()
的核心邏輯。
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
// 向前找到第一個過時條目
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i; // 記錄前一個過時條目的位置
// Find either the key or trailing null slot of run, whichever occurs first
// 向後查找,直到找到 key 或者 空 Entry
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
// 若是在向後查找過程當中發現 key 相同的 entry 就覆蓋而且和過時 entry 進行交換
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
// 若是在查找過程當中還未發現過時 entry,那麼就以當前位置做爲 cleanSomeSlots 的起點
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
// 若是向前未搜索到過時 entry,而在向後查找過程遇到過時 entry 的話,後面就以此時這個位置
// 做爲起點執行 cleanSomeSlots
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
// 若是在查找過程當中沒有找到能夠覆蓋的 entry,則將新的 entry 插入在過時 entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
// 在上面的代碼運行過程當中,找到了其餘的過時條目
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
複製代碼
看起來挺累人的。在我理解,replaceStaleEntry
只是作一個標記的做用,在各類狀況下最後都會調用 cleanSomeSlots
來真正的清理過時條目。
你能夠看到 ``
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i); // 須要清理的 Entry
}
} while ( (n >>>= 1) != 0);
return removed;
}
複製代碼
參數 n
表示掃描控制。初始狀況下掃描 log2(n)
次,若是遇到過時條目,會再掃描 log2(table.length)-1
次。在 set()
方法中調用,參數 n
表示元素的個數。在 replaceStaleEntry
中調用,參數 n
表示的是數組 table
的長度。
注意 do 循環裏面的判斷條件:e != null && e.get() == null
,仍是那些 Entry 不爲空,key 爲空的過時條目。發現過時條目以後,調用 expungeStaleEntry()
去清理。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
// 清空 staleSlot 處的 過時 entry
// 將 value 置空,保證不會由於這裏的強引用形成 memory leak
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
// 繼續搜索直到遇到 tab 中的空 entry
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) { // 搜索過程當中遇到過時條目,直接清理
e.value = null;
tab[i] = null;
size--;
} else {
// key 尚未被回收
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i; // 此時從 staleSlot 到 i 之間不存在過時條目
}
複製代碼
直接將 entry.value
和 entry
都置空,消除內存泄露的隱患。注意這裏僅僅只是置空,並非回收對象。由於你不知道 value
在外部的引用狀況,只須要管好本身的引用就能夠了。
除此以外,不甘寂寞的 expungeStaleEntry()
又發起了一次掃描,直到碰到空 Entry未知。期間遇到的過時 Entry 要置空。
整個 set()
方法就看完了,原理很簡單,可是其中關於內存泄漏的預防處理十分複雜,看的我一度放棄了,也讓我對源碼閱讀產生了一些疑問。有些時候是否是沒有必要逐行去玩去徹底理解?好比這一系列關於內存泄露的處理,核心思想就是清理 Entry 不爲 null 但 key 爲 null 的過時條目。理解了核心思想,對於其中複雜的細節處理是否是沒有必要去深究?不知道你怎麼看,歡迎在評論區寫下你的見解。
下面來看一看 getgetEntry
方法。
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key) // 直接命中
return e;
else
// 未直接命中,線性探測,繼續日後找
return getEntryAfterMiss(key, i, e);
}
複製代碼
getEntry()
比較粗暴,上來直接根據哈希值查找 table 數組,若是直接命中,就返回。未直接命中,調用 getEntryAfterMiss()
繼續查找。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 向後查找直到遇到空 entry
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key) // get it
return e;
if (k == null) // key 等於 null,清理過時 entry
expungeStaleEntry(i);
else
i = nextIndex(i, len); // 繼續向後查找
e = tab[i];
}
return null;
}
複製代碼
調用 nextIndex()
向後查找,直到遇到 空 Entry,也就是隊尾:
k==key
,說明找到了對應 Entryk==null
,說明遇到了過時 Entry,調用 expungeStaleEntry()
處理對過時 Entry 的處理真的是無處不在,就是爲了最大程度的下降內存泄漏發生的概率。那麼有沒有什麼一勞永逸的辦法呢?那就是 ThreadLocalMap
的 remove()
方法。
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
複製代碼
直接清除當前 ThreadLocal 對應的 Entry,根本上避免了發生內存泄露。因此,當咱們再也不須要使用 ThreadLocal 中的相應數據時,調用一下 remove()
方法確定是個好習慣。
雖然在長期存活的線程(例如線程池)中使用 ThreadLocal
併發生內存泄漏是一個小几率事件,但 JDK 開發者卻爲此多寫了不少代碼。咱們在使用中也要多加註意,仔細考慮是否會涉及到內存泄露的問題。
最後說說在網上看到的一個觀點,ThreadLocal 比 Synchronized 更適合解決線程同步問題。
首先這個問題自己就不是那麼嚴謹。ThreadLocal
是用來解決線程同步問題的嗎?表面上看,ThreadLocal
的機制的確是線程安全的,但它並非爲了解決多線程訪問同一個變量的競爭問題,而是給每個線程都提供單獨的變量,有些文章稱之爲 數據備份,但它們並非備份,每個都是獨立存在的,互不干擾,並不存在什麼同步問題。
ThreadLocal
和 Synchronized
的應用場景也是千差萬別的。例如銀行的轉帳場景,涉及多個帳戶同時轉帳的多線程同步問題,ThreadLocal
根本就無法解決,即便每一個線程都單獨保存着用戶的餘額也無法解決併發問題。ThreadLocal
在 Android 中的典型應用就是 Looper
,每一個線程都有本身的 Looper
對象,它們都是獨立工做,互不干擾的。
關於 ThreadLocal 就說到這裏了。後續分享的方向主要集中在兩塊,一方面是 AOSP 源碼的閱讀和解析,另外一方面是 Kotlin 和 Java 相關特性的對比,敬請期待!
文章首發微信公衆號:
秉心說
, 專一 Java 、 Android 原創知識分享,LeetCode 題解。更多最新原創文章,掃碼關注我吧!