天天看點

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>,則每一小塊存儲區中就儲存了與特定線程關聯的Looper。

Android如何保證一個線程最多隻能有一個Looper?

3. ThreadLocal的内部實作原理

3.1 Thread、ThreadLocal和Values的關系

Thread的成員變量localValues代表了線程特定變量,類型為ThreadLocal.Values。由于線程特定變量可能會有多個,并且類型不确定,是以ThreadLocal.Values有一個table成員變量,類型為Object數組。這個localValues可以了解為二維存儲區中與特定線程相關的一列。

ThreadLocal類則相當于一個代理,真正操作線程特定存儲區table的是其内部類Values。

Android如何保證一個線程最多隻能有一個Looper?
Android如何保證一個線程最多隻能有一個Looper?

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

既然與特定線程相關,是以先擷取目前線程,然後擷取目前線程特定存儲,即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 = -;

    for (int index = key.hash & mask;; index = next(index)) {
        Object k = table[index];

        if (k == key.reference) {
            // Replace existing entry.
            table[index + ] = value;
            return;
        }

        if (k == null) {
            if (firstTombstone == -) {
                // Fill in null slot.
                table[index] = key.reference;
                table[index + ] = value;
                size++;
                return;
            }

            // Go back and replace first tombstone.
            table[firstTombstone] = key.reference;
            table[firstTombstone + ] = value;
            tombstones--;
            size++;
            return;
        }

        // Remember first tombstone.
        if (firstTombstone == - && k == TOMBSTONE) {
            firstTombstone = index;
        }
    }
}
           

從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 + ];
        }
    } else {
        values = initializeValues(currentThread);
    }

    return (T) values.getAfterMiss(this);
}
           

首先取出與線程相關的Values,然後在table中尋找ThreadLocal的reference對象在table中的位置,然後傳回下一個位置所存儲的對象,即ThreadLocal的值,在Looper這個例子中就是與目前線程關聯的Looper對象。

從set和get方法可以看出,其所操作的都是目前線程的localValues中的table數組,是以不同線程調用同一個ThreadLocal對象的set和get方法互不影響,這就是ThreadLocal為解決多線程程式的并發問題提供了一種新的思路。

4. 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模式的總體結構

Android如何保證一個線程最多隻能有一個Looper?

線程特定對象,相當于Looper。

線程特定對象集包含一組與特定線程相關聯的線程特定對象。每個線程都有自己的線程特定對象集。相當于ThreadLocal.Values。線程特定對象集可以存儲線上程内部或外部。Win32、Pthread和Java都對線程特定資料有支援,這種情況下線程特定對象集可以存儲線上程内部。

線程特定對象代理,讓用戶端能夠像通路正常對象一樣通路線程特定對象。如果沒有代理,用戶端必須直接通路線程特定對象集并顯示地使用鍵。相當于ThreadLocal<Looper>。

從概念上講,可将Thread-Specific Storage的結構視為一個二維矩陣,每個鍵對應一行,每個線程對應一列。第k行、第t列的矩陣元素為指向相應線程特定對象的指針。線程特定對象代理和線程特定對象集協作,向應用程式線程提供一種通路第k行、第t列對象的安全機制。注意,這個模型隻是類比。實際上Thread-Specific Storage模式的實作并不是使用二維矩陣,因為鍵不一定是相鄰整數。

Android如何保證一個線程最多隻能有一個Looper?

參考資料

  1. Thread-local storage
  2. 面向模式的軟體架構·卷2:并發和聯網對象模式