[轉]Android的ThreadLocal的工作原理
Looper
中有一個特殊的概念,那就是ThreadLocal
,ThreadLocal
並不是執行緒,它的作用是可以在每個執行緒中儲存資料。大家知道,Handler
建立的時候會採用當前執行緒的Looper
來構造訊息迴圈系統,那麼Handler
內部如何獲取到當前執行緒的Looper
呢?這就要使用ThreadLocal
了,ThreadLocal
可以在不同的執行緒之中互不干擾地儲存並提供資料。
ThreadLocal
是一個執行緒內部的資料儲存類,通過它可以在指定的執行緒中儲存資料,資料儲存以後,只有在指定執行緒中可以獲取到儲存的資料,對於其它執行緒來說無法獲取到資料。在日常開發中用到ThreadLocal
ThreadLocal
可以輕鬆地實現一些看起來很複雜的功能,這一點在Android的原始碼中也有所體現,比如Looper
、ActivityThread
以及AMS
中都用到了ThreadLocal
。具體到ThreadLocal
的使用場景,這個不好統一地來描述,一般來說,當某些資料是以執行緒為作用域並且不同執行緒具有不同的資料副本的時候,就可以考慮採用ThreadLocal
。比如對於Handler
來說,它需要獲取當前執行緒的Looper
,很顯然Looper
的作用域就是執行緒並且不同執行緒具有不同的Looper
,這個時候通過ThreadLocal
就可以輕鬆實現Looper
ThreadLocal
,那麼系統就必須提供一個全域性的雜湊表供Handler
查詢指定執行緒的Looper
,這樣一來就必須提供一個類似於LooperManager
的類了,但是系統並沒有這麼做而是選擇了ThreadLocal
,這就是ThreadLocal
的好處。
ThreadLocal
另一個使用場景是複雜邏輯下的物件傳遞,比如監聽器的傳遞,有些時候一個執行緒中的任務過於複雜,這可能表現為函式呼叫棧比較深以及程式碼入口的多樣性,在這種情況下,我們又需要監聽器能夠貫穿整個執行緒的執行過程,這個時候可以怎麼做呢?其實就可以採用ThreadLocal
,採用ThreadLocal
get
方法就可以獲取到監聽器。
介紹了那麼多ThreadLocal
的知識,可能還是有點抽象,下面通過實際的例子為大家演示ThreadLocal
的真正含義。首先定義一個ThreadLocal
物件,這裡選擇Boolean
型別的,如下所示:
private ThreadLocal<Boolean> mBooleanThreadLocal = new ThreadLocal<Boolean>();
然後分別在主執行緒
、子執行緒1
和子執行緒2
中設定和訪問它的值,程式碼如下所示:
mBooleanThreadLocal.set(true);
Log.d(TAG, "[Thread#main]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
new Thread("Thread#1") {
@Override
public void run() {
mBooleanThreadLocal.set(false);
Log.d(TAG, "[Thread#1]mBooleanThreadLocal="
+ mBooleanThreadLocal.get());
};
}.start();
new Thread("Thread#2") {
@Override
public void run() {
Log.d(TAG, "[Thread#2]mBooleanThreadLocal="
+ mBooleanThreadLocal.get());
};
}.start();
在上面的程式碼中,在主執行緒
中設定mBooleanThreadLocal
的值為true
,在子執行緒1
中設定mBooleanThreadLocal
的值為false
,在子執行緒2
中不設定mBooleanThreadLocal
的值,然後分別在3個執行緒中通過get
方法去mBooleanThreadLocal
的值,根據前面對ThreadLocal
的描述,這個時候,主執行緒
中應該是true
,子執行緒1
中應該是false
,而子執行緒2
中由於沒有設定值,所以應該是null
,安裝並執行程式,日誌如下所示:
D/TestActivity(8676):[Thread#main]mBooleanThreadLocal=true
D/TestActivity(8676):[Thread#1]mBooleanThreadLocal=false
D/TestActivity(8676):[Thread#2]mBooleanThreadLocal=null
從上面日誌可以看出,雖然在不同執行緒中訪問的是同一個ThreadLocal
物件,但是它們通過ThreadLocal
來獲取到的值卻是不一樣的,這就是ThreadLocal
的奇妙之處。結合這這個例子然後再看一遍前面對ThreadLocal
的兩個使用場景的理論分析,大家應該就能比較好地理解ThreadLocal
的使用方法了。ThreadLocal
之所以有這麼奇妙的效果,是因為不同執行緒訪問同一個ThreadLocal
的get
方法,ThreadLocal
內部會從各自的執行緒中取出一個數組,然後再從陣列中根據當前ThreadLocal
的索引去查找出對應的value
值,很顯然,不同執行緒中的陣列是不同的,這就是為什麼通過ThreadLocal
可以在不同的執行緒中維護一套資料的副本並且彼此互不干擾。
對ThreadLocal
的使用方法和工作過程做了一個介紹後,下面分析下ThreadLocal
的內部實現, ThreadLocal
是一個泛型類,它的定義為public class ThreadLocal<T>
,只要弄清楚ThreadLocal
的get
和set
方法就可以明白它的工作原理。
首先看ThreadLocal
的set
方法,如下所示:
public void set(T value) {
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values == null) {
values = initializeValues(currentThread);
}
values.put(this, value);
}
在上面的set
方法中,首先會通過values
方法來獲取當前執行緒中的ThreadLocal
資料,如果獲取呢?其實獲取的方式也是很簡單的,在Thread
類的內容有一個成員專門用於儲存執行緒的ThreadLocal
的資料,如下所示:ThreadLocal.Values localValues
,因此獲取當前執行緒的ThreadLocal
資料就變得異常簡單了。如果localValues
的值為null
,那麼就需要對其進行初始化,初始化後再將ThreadLocal
的值進行儲存。下面看下ThreadLocal
的值到底是怎麼localValues
中進行儲存的。在localValues
內部有一個數組:private Object[] table
,ThreadLocal
的值就是存在在這個table
陣列中,下面看下localValues
是如何使用put
方法將ThreadLocal
的值儲存到table
陣列中的,如下所示:
void put(ThreadLocal<?> key, Object value) {
cleanUp();
// Keep track of first tombstone. That's where we want to go back
// and add an entry if necessary.
int firstTombstone = -1;
for (int index = key.hash & mask;; index = next(index)) {
Object k = table[index];
if (k == key.reference) {
// Replace existing entry.
table[index + 1] = value;
return;
}
if (k == null) {
if (firstTombstone == -1) {
// Fill in null slot.
table[index] = key.reference;
table[index + 1] = value;
size++;
return;
}
// Go back and replace first tombstone.
table[firstTombstone] = key.reference;
table[firstTombstone + 1] = value;
tombstones--;
size++;
return;
}
// Remember first tombstone.
if (firstTombstone == -1 && k == TOMBSTONE) {
firstTombstone = index;
}
}
}
上面的程式碼實現資料的儲存過程,這裡不去分析它的具體演算法,但是我們可以得出一個儲存規則,那就是ThreadLocal
的值在table陣列中的儲存位置總是為ThreadLocal
的reference
欄位所標識的物件的下一個位置,比如ThreadLocal
的reference
物件在table
陣列的索引為index
,那麼ThreadLocal
的值在table
陣列中的索引就是index+1
。最終ThreadLocal
的值將會被儲存在table
陣列中:table[index + 1] = value。
上面分析了ThreadLocal
的set
方法,這裡分析下它的get
方法,如下所示:
public T get() {
// Optimized for the fast path.
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values != null) {
Object[] table = values.table;
int index = hash & values.mask;
if (this.reference == table[index]) {
return (T) table[index + 1];
}
} else {
values = initializeValues(currentThread);
}
return (T) values.getAfterMiss(this);
}
可以發現,ThreadLocal
的get
方法的邏輯也比較清晰,它同樣是取出當前執行緒的localValues
物件,如果這個物件為null
那麼就返回初始值,初始值由ThreadLocal
的initialValue
方法來描述,預設情況下為null
,當然也可以重寫這個方法。
如果localValues
物件不為null
,那就取出它的table
陣列並找出ThreadLocal
的reference
物件在table
陣列中的位置,然後table
陣列中的下一個位置所儲存的資料就是ThreadLocal
的值。
從ThreadLocal
的set
和get
方法可以看出,它們所操作的物件都是當前執行緒的localValues
物件的table
陣列,因此在不同執行緒中訪問同一個ThreadLocal
的set
和get
方法,它們對ThreadLocal
所做的讀寫操作僅限於各自執行緒的內部,這就是為什麼ThreadLocal
可以在多個執行緒中互不干擾地儲存和修改資料,理解ThreadLocal
的實現方式有助於理解Looper
的工作原理。
程式碼分析基於L
來張L上的ThreadLocal圖:
在Android7.0之後ThreadLocal的實現跟JDK7上保持了一致,來張JDK7上的ThreadLocal圖吧
個人感覺這樣的修改,跟那個WeakReference導致的GC有關,以後再來求證下吧。[TODO]
參考文章:
https://blog.csdn.net/singwhatiwanna/article/details/48350919