ThreadLocal是一個線程內部的數據存儲類,可以在指定線程存儲和讀取數據,而數據對於其他線程是不可見的。日常開發中通常會比較少用到ThreadLocal,但是在一些特殊場景可以輕鬆實現一些比較複雜的需求。我們經常接觸到的Android消息機制正是使用了ThreadLocal存儲不同線程的Looper對象。
基本使用方法
我們先看看它的基本使用方法。
private static final String TAG = "ThreadLocal";
private ThreadLocal<String> mThreadLocal = new ThreadLocal<>();
private void testThreadLocal() {
mThreadLocal.set("main_thread");
new Thread("sub_thread_1") {
@Override
public void run() {
Log.d(TAG, "sub_thread_1 before: " + mThreadLocal.get());
mThreadLocal.set("sub_thread_1");
Log.d(TAG, "sub_thread_1 after: " + mThreadLocal.get());
}
}.start();
new Thread("sub_thread_2") {
@Override
public void run() {
Log.d(TAG, "sub_thread_2 before: " + mThreadLocal.get());
mThreadLocal.set("sub_thread_2");
Log.d(TAG, "sub_thread_2 after: " + mThreadLocal.get());
}
}.start();
Log.d(TAG, "main_thread : " + mThreadLocal.get());
}
我們分別在主線程給ThreadLocal設置一個初始值,然後創建兩個線程,在線程內部先嚐試讀取該ThreadLocal的值,然後在線程內部重新賦值,然後再次嘗試讀取其值。打印出來的結果如下
可以明確看到線程之間ThreadLocal存儲的值是互相獨立,當前線程無法訪問其他線程存儲的值。
源碼分析
接下來我們來分析一下內部實現原理,既然ThreadLocal是一個數據存儲類,那麼set和get方法就是它的最核心方法。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
從源碼可以得出,ThreadLocal的set和get方法實際是調用其靜態內部類ThreadLocalMap的set和getEntry方法,而ThreadLocalMap的實例則是通過調用getMap方法獲取到的,下面是獲取和創建ThreadLocalMap實例的源碼。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
上述代碼表明ThreadLocalMap的實例是存儲在代表當前線程的Thread類對象中,而ThreadLocal的get方法中的判空操作也說明每個線程只創建並持有一個ThreadLocalMap實例。但是我們在開發中可以創建任意數量的ThreadLocal實例,說明對於每個線程來說,ThreadLocalMap和ThreadLocals是一對多的關係,那麼必然需要解決存在的衝突問題。
在分析ThreadLocalMap的set,getEntry以及如何解決衝突之前,我們先來分析一下ThreadLocalMap的結構。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
..........
}
從上訴源碼可以得知
- ThreadLocalMap有自己的Entry類,繼承至弱引用類,key是ThreadLocal類,成員變量value則是真正我們需要存儲的值。
- Entry類以數組形式存在,且數組容量初始爲16。
- size記錄了Entry類實際的數量,一旦超過了臨界值(臨界值通過threshold計算而來)會進行擴容(2倍)。
ThreadLocalMap雖然不是Map的實現類,但是卻實現了類似Map的功能。
ThreadLocalMap set方法源碼如下:
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);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
for循環是這段代碼核心,我們首先來理解它的循環條件。
- Entry e = tab[i]; 初始值
- e != null;循環條件,如果e == null 則終止循環
- e = tab[i = nextIndex(i, len)] 獲取下一個Entry
循環體內部邏輯如下
- 取出Entry(弱引用類型)中存儲的ThreadLocal對象。
- 如果存儲的ThreadLocal對象和當前作爲參數傳遞過來的的key是同一對象,那麼說明命中緩存,直接更新value並返回。
- 如果從Entry中取出的是null, 說明該ThreadLocal已經被GC回收,替換當前的Entry對象並返回。
- 如果不滿足2,3條件,則循環獲取下一個Entry。
如果在循環體內並未return,根據循環終止條件,說明找到一個空位,則新建一個Entry,有效Entry數量加1。
最後的cleanSomeSlots方法主要作用是嘗試清除已經被GC回收的ThreadLocal 緩存,如果清除失敗(都沒有被GC回收)且當前有效Entry數量已經達到臨界值,則進行擴容。
set方法基本的邏輯已經分析完畢,擴容和清除部分的代碼和set中的for循環類似,不再進行深入分析。但是還遺漏一個問題,初始位置 i 是如何計算出來的,如何保證不同的ThreadLocal計算出來的i是唯一的。
int i = key.threadLocalHashCode & (len-1);
從源碼得知,初始位置是由數組長度以及ThreadLocal的成員變量 threadLocalHashCode決定,數組長度在不進行擴容的前提下是不變的,所以i取決於每個ThreadLocal實例的threadLocalHashCode值。
private final int threadLocalHashCode = nextHashCode();
/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
threadLocalHashCode是final類型的成員變量,通過調用nextHashCode()獲取,而nextHashCode()方法則是通過一個原子類AtomicInteger類型的靜態變量調用getAndAdd方法返回的(getAndAdd() 方法作用是增加值同時返回舊值)。
也就是說每次創建ThreadLocal實例,threadLocalHashCode就會增長HASH_INCREAMENT,從而保證每一個ThreadLocal擁有唯一的threadLocalHashCode,在計算初始位置的時候就避免的衝突。
ThreadLocalMap的getEntry()方法就簡單很多。
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);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
直接去找初始位置的Entry,如果匹配則返回對應value,如果不匹配,再循環查找table裏的Entry。
注意事項
在某些場景下,ThreadLocal可以極大地減小多線程需求的編碼複雜度,但是當ThreadLocal和線程池結合使用時,必須注意潛在的問題。先看以下例子。
private static final String TAG = "ThreadLocal";
private ThreadLocal<String> mThreadLocal = new ThreadLocal<>();
private Executor mExecutors = Executors.newSingleThreadExecutor();
private void testThreadLocal() {
mExecutors.execute(new Runnable() {
@Override
public void run() {
mThreadLocal.set("runnable_1");
Log.d(TAG, "runnable_1 : " + mThreadLocal.get());
}
});
mExecutors.execute(new Runnable() {
@Override
public void run() {
Log.d(TAG, "runnable_2 : " + mThreadLocal.get());
}
});
}
輸出log如下
可以看到儘管第二個任務並未給ThreadLocal設置值,但是卻可以直接從中取出有效值,且有效值和前一個任務設置的值一樣。
這是因爲線程池中的線程是複用的,所以會導致不同任務之間操作的實際是同一個ThreadLocal緩存,所以在線程池中如果明確ThreadLocal不再使用後需要手動remove,避免對後續任務造成干擾。