天天看點

Android消息機制之ThreadLocal的工作原理

轉載自:https://blog.csdn.net/singwhatiwanna/article/details/48350919

從開發的角度來說,Handler是Android消息機制的上層接口,這使得開發過程中隻需要和Handler互動即可。Handler的使用過程很簡單,通過它可以輕松的将一個任務切換到Handler所在的線程中去執行。很多人認為Handler的作用是更新UI,這說的的确沒錯,但是gengxUI僅僅是Handler的一個特殊的使用場景,具體來說是這樣的:有時候需要在子線程中進行耗時的IO操作,可能讀取檔案或者通路網絡,當耗時操作完成之後,可能需要在UI上做一些改變,由于Anderoid開發規範的限制,我們并不能在子線程中通路UI控件,否則就會觸發程式異常,這個時候通過Handler就可以将更新Ui的操作切換到主線程中執行,是以,本質上來說,Handler并不是專門用于更新UI的,它隻是常被大家用來更新UI。

Android的消息機制主要是指Handler的運作機制,Handler的運作需要底層的MessageQueue和Looper的支撐。MessageQueue是消息隊列,通過單連結清單的資料結構來維護消息清單。Handler是消息輔助類,向消息池中發送各種消息事件和處理消息事件。Looper不斷從消息隊列中讀取消息,按分發機制将消息分發給目标處理者。

Looper中還有一個特殊的概念,就是ThreadLocal,ThreadLocal并不是線程,他的作用是可以在每個線程中存儲資料。 Handler建立的時候會采用目前線程的Looper來構造消息循環系統,那麼handler内部如何擷取到目前線程的Looper呢?這需要使用Threadlocal,Threadlocal可以在不同的線程之中互不幹擾地存儲并提供資料,通過Threadlocal可以輕松擷取每個線程的Looper。需要注意的是,線程時預設沒有Looper的,如果需要使用Handler就必須為線程建立Looper。而Ui線程被建立時就會初始化looper,這也是在主線程中預設可以使用handle的原因。

ThreadLocal

ThreadLocal是一個線程内部的資料存儲類,通過它可以在指定的線程中存儲資料,資料存儲以後,隻有在指定線程中可以擷取到存儲的資料,對于其他線程來說無法擷取到資料。在日常開發中用到ThreadLocal的地方比較少,但是在某些特殊的場景下,通過ThreadLocal可以輕松的實作一些看起來很複雜的功能。這一點在android的源碼中也有所展現,比如Looper、ActivityTHread以及AMS中都用到了ThreadLocal。具體到ThreadLocal的使用場景,這個不好統一的來描述,一般來說,當某些資料是以線程為作用域并且不同線程具有不同的資料副本的時候,就可以考慮采用ThreadLocal。比如對于Handler來說,它需要擷取目前線程的Looper,很顯然Looper的作用域就是線程并且不同線程具有不同的Looper,這個時候通過ThreadLocal就可以輕松實作Looper線上程中的存取,如果不采用ThreadLocal,那麼系統就必須提供一個全局的哈希表供Handler執行線程的Looper,這樣一來就必須提供一個類似于LooperManager的類了,但是系統并沒有這麼做而是選擇了ThreadLocal,這就是ThreadLocal的好處。

ThreadLocal的另一使用場景是複雜場景下的對象傳遞,比如監聽器的傳遞,有些時候一個線程中的任務過于複雜,這可能表現為函數調用棧比較深以及代碼入口的多樣性。這種情況下,我們又需要監聽器能夠貫穿整個過程的執行過程,這個時候可以怎麼做呢?其實可以采用ThreadLocal,采用ThreadLocal可以讓監聽器作為線程内的全局對象而存在,線上程内部隻要通過get方法就可以擷取到監聽器。而如果不采用ThreadLocal,那麼我們能想到的可能是如下兩種方法:第一種方法是将監聽器通過參數的形式在函數調用棧中進行傳遞,第二種方法是将監聽器作為靜态變量供線程通路。上面兩種方法都是由局限性的。第一種方法的問題是當函數調用棧很深的時候,通過函數參數來傳遞監聽器這幾乎是不可能接受的,這會讓程式的設計看起來很糟糕。第二種方法是可以接受的,但這種狀态是不具有可擴充性的,比如如果同時有兩個線程在執行,那麼就需要提供兩個靜态的監聽器對象,如果有10個線程在并發執行呢?提供10個靜态的監聽器對象?這顯然是不可思議的,而采用ThreadLocal每個監聽器對象都在自己的線程内部存儲,就不會有方法2這種問題。

執行個體

private ThreadLocal<Boolean>mBooleanThreadLocal = new ThreadLocal<Boolean>();
           

然後分别在主線程、子線程1和子線程2中設定和通路它的值,代碼如下。

mBooleanThreadLocal.set(true);
Log.d(TAG, "[Thread#main]mBooleanThreadLocal=" + mBooleanThreadLocal.get());

new Thread("Thread#1") {
    @Override
    public void run() {
        mBooleanThreadLocal.set(false);
        Log.d(TAG, "[Thread#1]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
    };
}.start();

new Thread("Thread#2") {
    @Override
    public void run() {
        Log.d(TAG, "[Thread#2]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
    };
}.start();
           

在上面代碼中,在主線程中設定mBooleanThreadLocal的值為true,在子線程1中設定mBooleanThreadLocal的值為false,在子線程2中不設定mBooleanThreadLocal的值,然後分别在3個線程中通過get方法區mBooleanThreadLocal的值,運作結果如下:

D/TestActivity():[Thread#main]mBooleanThreadLocal=true

D/TestActivity():[Thread#]mBooleanThreadLocal=false

D/TestActivity():[Thread#]mBooleanThreadLocal=null
           

從上面日志可以看出,雖然在不同線程中通路的是同一個ThreadLocal對象,但是他們通過ThreadLocal來擷取到的值卻是不一樣的,這就是ThreadLocal的奇妙之處。因為不同線程通路同一個THreadlocal的get方法,THreadLocal内部會從格子的線程中取出一個數組,然後再從數組中根據目前ThreadLocal的索引去查找出對應的value值。很顯然,不同的線程中數組是不同的,這就是為什麼通過ThreadLocal可以在不同的線程中維護一套資料的副本并且彼此互不幹擾。

Threadlocal内部實作

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

在set方法中,首先要通過values方法來擷取目前線程中的Threadlocal資料,在Thread類的内容中有一個專門的成員用于存儲線程的Threadlocal的資料,如下所示:TreadLocal.Values localValues.是以獲得目前線程的ThreadLocal資料變得異常簡單了。如果localValues的值為null,那麼就需要對其初始化,初始化後再将ThreadLocal的值進行存儲。下面看下THreadLocal的值到底是怎麼在localValues中進行存儲的。在localValues内部有一個數組:private Object[] table,ThreadLocal的值就是存在這個table數組中,下面看下localValues是如何使用put方法将ThreadLocal的值存儲到table數組中的,如下所示:

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

上面的代碼實作資料的存儲過程,這裡不去分析它的具體算法,但是我們可以得出一個存儲規則,那就是ThreadLocal的值在table數組中的存儲位置總是為ThreadLocal的reference字段所辨別的對象的下一個位置,比如ThreadLocal的reference對象在table數組中的索引為index,那麼ThreadLocal的值在table數組中的索引就是index+1,最終ThreadLocal的值将會被存儲在table數組中,table[index+1] = value.

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

可以發現,ThreadLocal的get方法的邏輯也比較清晰,它同樣是取出目前線程的localValues對象,如果這個對象為null那麼就傳回初始值,初始值由ThreadLocal的initialValue方法來描述,預設情況下為null,當然也可以重寫這個方法,它的預設實作如下所示:

/**
 * Provides the initial value of this variable for the current thread.
 * The default implementation returns {@code null}.
 *
 * @return the initial value of the variable.
 */
protected T initialValue() {
    return null;
}
           

如果localValues對象不為null,那就取出它的table數組并找出ThreadLocal的reference對象在table數組中的位置,然後table數組中的下一個位置所存儲的資料就是ThreadLocal的值。

從ThreadLocal的set和get方法可以看出,它們所操作的對象都是目前線程的localValues對象的table數組,是以在不同線程中範文同一個ThreadLocal的set和get方法,它們對ThreadLocal所作的讀寫操作僅限于各自線程的内部,這就是為什麼ThreadLocal可以在多個線程中互不幹擾地存儲和修改資料。