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

augfun發表於2020-12-11

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.百度轉載的分析:

新建立的執行緒裡進行 Looper.prepare() 操作,因為 Looper 的構造方法是私有的

 

所以要想建立 Looper 物件,還需使用 prepare() 方法建立

 

 

而 prepare() 方法的實質則是:

建立 Looper 物件將建立好的 Looper 物件儲存到 ThreadLocal 物件裡注意 : ThreadLocal 物件在建立 Looper 的時候建立的。

 

注意看 ThreadLocal 儲存 Looper 的處理方式,是保證 ThreadLocal 裡沒有其他的 Looper,只允許有一份 Looper。

這就是為什麼同一個 Thread 執行緒裡只能有一個 Looper 物件的原因。

這就解答了題目的問題。

接下來,我們來看一下為什麼 Looper 要由 ThreadLocal 保管呢?

 

 

這是 ThreadLocal 原始碼中對其的介紹,翻譯過來就是 ThreadLocal 提供了針對於單獨的執行緒的區域性變數,並能夠使用 set()、get() 方法對這些變數進行設定和獲取,並且能夠保證這些變數與其他執行緒相隔離。

換句話說,通過使用 threadLocal 儲存物件,執行緒和執行緒之間的彼此的資料就會隔離起來,從而保證了彼此執行緒的資料安全和獨立性。

我們來看一下 ThreadLocal 的 get 方法:

 

由上圖我們看到,get 方法是通過當前的執行緒thread,取到一個 ThreadLocalMap 物件(這個就把他當做 map 集合對待即可)。

如果得到的map 是空,則進行這個 map 的初始化:

setInitialValue()。

 

 

而初始化的方式也很簡單,就是建立一個以 ThreadLocal 自身為 key,存入的物件為 value 的 ThreadLocalMap 物件,然後把這個 ThreadLocalMap 存到執行緒 thread 裡面(方便與執行緒進行繫結)。

如果得到的 map 不為空

 

則從 ThreadLocalMap 中獲取對應的儲存物件,如果沒有向 ThreadLocal 裡呼叫 set() 方法,這個時候呼叫 get() 方法返回的值就是 null。

回想一下 Looper 的 prepare() 方法

 

就很符合這個邏輯。

prepare() 方法會判斷 threadLocal 的 get() 方法,如果返回值不為 null,說明呼叫過了 threadLocal 的set() 方法了。

此時 set 方法其實也無需再看了,無非就是將需要儲存的物件當做value,當前的 threadLocal 的引用當做key,儲存進 threadLocalMap 裡面,然後將其指向執行緒thread。

所以總述一下,Looper 是通過利用 ThreadLocal 的資料隔離性將 Looper 物件儲存在對應的執行緒中,保證每一個執行緒只能建立一個 Looper 物件。

參考地址:https://baijiahao.baidu.com/s?id=1672811852535456858&wfr=spider&for=pc

3. 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。
這裡寫圖片描述

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

既然與特定執行緒相關,所以先獲取當前執行緒,然後獲取當前執行緒特定儲存,即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;
        }
    }
}

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

首先取出與執行緒相關的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模式的總體結構

這裡寫圖片描述

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

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

相關文章