對ThreadLocal在Handler中的應用的一些理解
前言
JDK原始碼的ThreadLocal類和Android SDK的ThreadLocal類細節略有不同,但原理和實現的功能是相同的。本文的程式碼均來自Android SDK原始碼。
看下Android SDK原始碼裡ThreadLocal的註釋:
/**
* Implements a thread-local storage, that is, a variable for which each thread
* has its own value. All threads share the same {@code ThreadLocal} object,
* but each sees a different value when accessing it, and changes made by one
* thread do not affect the other threads. The implementation supports
* {@code null} values.
*
* @see java.lang.Thread
* @author Bob Lee
*/
就不翻譯了。我的理解,ThreadLocal是“執行緒內部儲存”,它不是一個執行緒,而是用於儲存物件,執行緒內部獲取到的這個物件是唯一的,而不同執行緒獲取的這個物件則是不同的物件。
也即,這個物件的作用域是執行緒,而不是平常我們使用的包內,類內或者方法內等。
說個題外話,原始碼註釋還有這麼一句:
/* Thanks to Josh Bloch and Doug Lea for code reviews and impl advice. */
作者特意在原始碼註釋裡表示了對Josh Bloch和Doug Lea的感謝,謝謝他們的review和建議。原來這部分程式碼還有這兩位Java大牛的參與~不多說,下面正式開始分析。
Handler與ThreadLocal
在Android中,一個典型的ThreadLocal的使用場景就是Handler類的實現原始碼裡,ThreadLocal用於儲存當前執行緒的Looper物件,從而把執行緒和Looper物件實現一一對應的關係。
下面具體看下原始碼實現:
Looper類裡,有個靜態變數sThreadLocal,它儲存的物件是Looper物件:
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
而我們都知道,如果想在一個新執行緒裡自定義Handler並使用的話,就必須先呼叫Looper.prepare(),然後定義Handler之後,再呼叫Looper.loop()。
Looper.prepare()裡有這麼一行程式碼:
sThreadLocal.set(new Looper(quitAllowed));
這一行的作用,就是把當前執行緒與新建立的Looper物件對應起來,那具體是怎麼實現對應的呢?
如何保證Looper物件線上程裡是唯一的
Looper的唯一的構造方法是在prepare()裡呼叫的,當建立了一個Looper物件之後,立刻會被ThreadLocal的set()方法作為引數傳入處理。
來看看ThreadLocal的set方法做了什麼:
public void set(T value) {
Thread currentThread = Thread.currentThread();//得到當前執行緒
Values values = values(currentThread);// 得到當前執行緒的localValues變數的值
if (values == null) {//首次執行時,會進入這裡
values = initializeValues(currentThread);//新建立一個Values物件
}
values.put(this, value);
}
1.來看下values()方法;
Values values(Thread current) {
return current.localValues;
}
values()方法就是為了得到當前執行緒的localValues變數的值。那麼這個localValues的值是什麼?又是時候被賦值的呢?
看它在Thread類裡的定義;
ThreadLocal.Values localValues;
localValues它是個ThreadLocal的內部類Values型別的一個變數,許可權是包內可訪問,同時也沒有對它的get和set方法。那麼ThreadLocal的set方法首次獲取當前執行緒的localValues就為null, 也就會走到initializeValues()方法裡。
2.看下這個initializeValues()方法:
Values initializeValues(Thread current) {
return current.localValues = new Values();
}
當前執行緒的localValues物件就是在這裡建立的。
3.獲取values物件後,最後是values.put(this, value),這個this就是sThreadLocal物件,value就是新建立的Looper物件。
這個put()方法就不分析了。可以把Values類當成一個Map(實際上Java的實現就是如此,而Android SDK裡用的是一個Object陣列,下標偶數的作為key,奇數下標的作為value,從而實現key 和value的一一對應。這部分可以理解為,每個執行緒有一個map, 這個map儲存的每個鍵值對, Threadlocal物件為鍵, Looper物件為值。而這個執行緒裡如果定義了其他ThreadLocal物件,也存在這個map裡
,具體可以看下ThreadLocal的Values內部類的實現)。
也就是說, 對ThreadLocal物件的處理,實際上就是對當前執行緒的localValues物件裡的Object陣列的處理,因此操作就侷限在了執行緒內部,也就實現了ThreadLocal可以在不同的執行緒儲存不同的物件。
上面我們講到,Looper的唯一的構造方法是在prepare()裡呼叫的,當建立了一個Looper物件之後,立刻會被ThreadLocal的set()方法作為引數傳入處理。
那麼,執行緒會不會存在多個Looper物件,即Looper的構造方法會被多次呼叫呢?答案是不會。如果需要多個Looper物件,那就得多次呼叫Looper.prepare()。
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
上面是Looper的prepare()的原始碼,首次呼叫Looper.prepare()時,sThreadLocal鍵值對已經生成。當再次進入這個prepare()方法時,先判斷sThreadLocal.get() 的值,看下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() 和 set()可以對照檢視。都是先獲取當前執行緒,然後獲取執行緒values的值,對values進行處理。在走到get()方法裡的時候,如果之前呼叫過set(),那麼values()已經不為null了,因此會返回sThreadLocal作為鍵,所儲存的Looper物件。
sThreadLocal.get()不為null,那麼Looper.prepare()就會丟擲異常了。也就是重複呼叫Looper.prepare()會丟擲異常,而不會建立多個Looper物件。
既然一個執行緒有隻一個Looper物件,而預設會有一個儲存此Looper物件的ThreadLocal。那麼有個疑問:儲存Looper物件時,所用的鍵值對的形式,為什麼不用當前執行緒作為鍵呢?我的理解是,一個執行緒可能有多個ThreadLocal物件(這些ThreadLocal物件我們可以自己建立),因此,執行緒和ThreadLocal是一對多的關係,如果把執行緒作為鍵的話,無法儲存不同的物件。
獲取執行緒內唯一的Looper物件
上面的分析中,我們可以知道,一個執行緒裡,sThreadLocal儲存的Looper物件是唯一的,怎麼獲取這個執行緒唯一的Looper物件呢?
Looper類提供了一個myLooper()方法,用於獲取Looper物件:
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
這也是獲取looper物件唯一的方法:從sThreadLocal進行獲取,因此也保證了獲取到的Looper物件就是sThreadLocal所儲存的物件,是此執行緒唯一的物件。