1. 程式人生 > >對ThreadLocal在Handler中的應用的一些理解

對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所儲存的物件,是此執行緒唯一的物件。