1. 程式人生 > >Android如何保證一個執行緒最多隻能有一個Looper?

Android如何保證一個執行緒最多隻能有一個Looper?

  1. 如何建立Looper?
    Looper的構造方法為private,所以不能直接使用其構造方法建立。
private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

要想在當前執行緒建立Looper,需使用Looper的prepare方法,Looper.prepare()。
如果現在要我們來實現Looper.prepare()這個方法,我們該怎麼做?我們知道,Android中一個執行緒最多隻能有一個Looper,若在已有Looper的執行緒中呼叫Looper.prepare()會丟擲RuntimeException(“Only one Looper may be created per thread”)。面對這樣的需求,我們可能會考慮使用一個HashMap,其中Key為執行緒ID,Value為與執行緒關聯的Looper,再加上一些同步機制,實現Looper.prepare()這個方法,程式碼如下:

public class Looper {

    static final HashMap<Long, Looper> looperRegistry = new HashMap<Long, Looper>();

    private static void prepare() {
        synchronized(Looper.class) {
            long currentThreadId = Thread.currentThread().getId();
            Looper l = looperRegistry.get(currentThreadId);
            if (l != null)
                throw new RuntimeException("Only one Looper may be created per thread");
            looperRegistry.put(currentThreadId, new Looper(true));
        }
    }
    ...
}

上述方法對Looper.class物件進行了加鎖,這些加鎖開銷有可能造成效能瓶頸。
有沒有更好的方法實現Looper.prepare()方法?看一看Android的中Looper的原始碼。

public class Looper {

    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

    public static void prepare() {
       prepare(true);
    }

    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));
    }
    ...
}

prepare()方法中呼叫了ThreadLocal的get和set方法,然而整個過程沒有新增同步鎖,Looper是如何實現執行緒安全的?
2. ThreadLocal
ThreadLocal位於java.lang包中,以下是JDK文件中對該類的描述

Implements a thread-local storage, that is, a variable for which each thread has its own value. All threads share the same 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 null values.

大致意思是,ThreadLocal實現了執行緒本地儲存。所有執行緒共享同一個ThreadLocal物件,但不同執行緒僅能訪問與其執行緒相關聯的值,一個執行緒修改ThreadLocal物件對其他執行緒沒有影響。

ThreadLocal為編寫多執行緒併發程式提供了一個新的思路。如下圖所示,我們可以將ThreadLocal理解為一塊儲存區,將這一大塊儲存區分割為多塊小的儲存區,每一個執行緒擁有一塊屬於自己的儲存區,那麼對自己的儲存區操作就不會影響其他執行緒。對於ThreadLocal,則每一小塊儲存區中就儲存了與特定執行緒關聯的Looper。

  1. ThreadLocal的內部實現原理
    3.1 Thread、ThreadLocal和Values的關係
    Thread的成員變數localValues代表了執行緒特定變數,型別為ThreadLocal.Values。由於執行緒特定變數可能會有多個,並且型別不確定,所以ThreadLocal.Values有一個table成員變數,型別為Object陣列。這個localValues可以理解為二維儲存區中與特定執行緒相關的一列。
    ThreadLocal類則相當於一個代理,真正操作執行緒特定儲存區table的是其內部類Values。

3.2 set方法
public void set(T value) {
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values == null) {
values = initializeValues(currentThread);
}
values.put(this, value);
}

Values values(Thread current) {
return current.localValues;
}
1
2
3
4
5
6
7
8
9
10
11
12
既然與特定執行緒相關,所以先獲取當前執行緒,然後獲取當前執行緒特定儲存,即Thread中的localValues,若localValues為空,則建立一個,最後將value存入values中。

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;
    }
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
從put方法中,ThreadLocal的reference和值都會存進table,索引分別為index和index+1。
對於Looper這個例子,
table[index] = sThreadLocal.reference;(指向自己的一個弱引用)
table[index + 1] = 與當前執行緒關聯的Looper。

3.3 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);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
首先取出與執行緒相關的Values,然後在table中尋找ThreadLocal的reference物件在table中的位置,然後返回下一個位置所儲存的物件,即ThreadLocal的值,在Looper這個例子中就是與當前執行緒關聯的Looper物件。

從set和get方法可以看出,其所操作的都是當前執行緒的localValues中的table陣列,所以不同執行緒呼叫同一個ThreadLocal物件的set和get方法互不影響,這就是ThreadLocal為解決多執行緒程式的併發問題提供了一種新的思路。

  1. ThreadLocal背後的設計思想Thread-Specific Storage模式
    Thread-Specific Storage讓多個執行緒能夠使用相同的”邏輯全域性“訪問點來獲取執行緒本地的物件,避免了每次訪問物件的鎖定開銷。

4.1 Thread-Specific Storage模式的起源
errno機制被廣泛用於一些作業系統平臺。errno 是記錄系統的最後一次錯誤程式碼。對於單執行緒程式,在全域性作用域內實現errno的效果不錯,但在多執行緒作業系統中,多執行緒併發可能導致一個執行緒設定的errno值被其他執行緒錯誤解讀。當時很多遺留庫和應用程式都是基於單執行緒編寫,為了在不修改既有介面和遺留程式碼的情況下,解決多執行緒訪問errno的問題,Thread-Specific Storage模式誕生。

4.2 Thread-Specific Storage模式的總體結構

執行緒特定物件,相當於Looper。
執行緒特定物件集包含一組與特定執行緒相關聯的執行緒特定物件。每個執行緒都有自己的執行緒特定物件集。相當於ThreadLocal.Values。執行緒特定物件集可以儲存線上程內部或外部。Win32、Pthread和Java都對執行緒特定資料有支援,這種情況下執行緒特定物件集可以儲存線上程內部。
執行緒特定物件代理,讓客戶端能夠像訪問常規物件一樣訪問執行緒特定物件。如果沒有代理,客戶端必須直接訪問執行緒特定物件集並顯示地使用鍵。相當於ThreadLocal。

從概念上講,可將Thread-Specific Storage的結構視為一個二維矩陣,每個鍵對應一行,每個執行緒對應一列。第k行、第t列的矩陣元素為指向相應執行緒特定物件的指標。執行緒特定物件代理和執行緒特定物件集協作,嚮應用程式執行緒提供一種訪問第k行、第t列物件的安全機制。注意,這個模型只是類比。實際上Thread-Specific Storage模式的實現並不是使用二維矩陣,因為鍵不一定是相鄰整數。

參考資料
Thread-local storage
面向模式的軟體架構·卷2:併發和聯網物件模式