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

來源:互聯網
上載者:User

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

提到訊息機制大家應該都不陌生,在日常開發中不可避免地要涉及到這方面的內容。從開發的角度來說,Handler是Android訊息機制的上層介面,這使得開發過程中只需要和Handler互動即可。Handler的使用過程很簡單,通過它可以輕鬆地將一個任務切換到Handler所在的線程中去執行。很多人認為Handler的作用是更新UI,這說的的確沒錯,但是更新UI僅僅是Handler的一個特殊的使用情境,具體來說是這樣的:有時候需要在子線程中進行耗時的IO操作,這可能是讀取檔案或者訪問網路等,當耗時操作完成以後可能需要在UI上做一些改變,由於Android開發規範的限制,我們並不能在主線程中訪問UI控制項,否則就會觸發程式異常,這個時候通過Handler就可以將更新UI的操作切換到主線程中執行。因此,本質上來說,Handler並不是專門用於更新UI的,它只是常被大家用來更新UI。

 

Android的訊息機制主要是指Handler的運行機制,Handler的運行需要底層的MessageQueue和Looper的支撐。MessageQueue的中文翻譯是訊息佇列,顧名思義它的內部儲存了一組訊息,其以隊列的形式對外提供插入和刪除的工作,雖然叫做訊息佇列,但是它的內部儲存結構並不是真正的隊列,而是採用單鏈表的資料結構來儲存訊息列表。Looper的中文翻譯為迴圈,在這裡可以理解為訊息迴圈,由於MessageQueue只是一個訊息的儲存單元,它不能去處理訊息,而Looper就填補了這個功能,Looper會以無限迴圈的形式去尋找是否有新訊息,如果有的話就處理訊息,否則就一直等待著。Looper中還有一個特殊的概念,那就是ThreadLocal,ThreadLocal並不是線程,它的作用是可以在每個線程中儲存資料。大家知道,Handler建立的時候會採用當前線程的Looper來構造訊息迴圈系統,那麼Handler內部如何擷取到當前線程的Looper呢?這就要使用ThreadLocal了,ThreadLocal可以在不同的線程之中互不干擾地儲存並提供資料,通過ThreadLocal可以輕鬆擷取每個線程的Looper。當然需要注意的是,線程是預設沒有Looper的,如果需要使用Handler就必須為線程建立Looper。大家經常提到的主線程,也叫UI線程,它就是ActivityThread,ActivityThread被建立時就會初始化Looper,這也是在主線程中預設可以使用Handler的原因。

 

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的這種問題。

介紹了那麼多ThreadLocal的知識,可能還是有點抽象,下面通過實際的例子為大家示範ThreadLocal的真正含義。首先定義一個ThreadLocal對象,這裡選擇Boolean類型的,如下所示:

private ThreadLocalmBooleanThreadLocal = new ThreadLocal();

然後分別在主線程、子線程1和子線程2中設定和訪問它的值,代碼如下所示:

mBooleanThreadLocal.set(true);Log.d(TAG, [Thread#main]mBooleanThreadLocal= + mBooleanThreadLocal.get());new Thread(Thread#1) {@Overridepublic void run() {mBooleanThreadLocal.set(false);Log.d(TAG, [Thread#1]mBooleanThreadLocal= + mBooleanThreadLocal.get());};}.start();new Thread(Thread#2) {@Overridepublic void run() {Log.d(TAG, [Thread#2]mBooleanThreadLocal= + mBooleanThreadLocal.get());};}.start();

在上面的代碼中,在主線程中設定mBooleanThreadLocal的值為true,在子線程1中設定mBooleanThreadLocal的值為false,在子線程2中不設定mBooleanThreadLocal的值,然後分別在3個線程中通過get方法去mBooleanThreadLocal的值,根據前面對ThreadLocal的描述,這個時候,主線程中應該是true,子線程1中應該是false,而子線程2中由於沒有設定值,所以應該是null,安裝並運行程式,日誌如下所示:

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

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

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

從上面日誌可以看出,雖然在不同線程中訪問的是同一個ThreadLocal對象,但是它們通過ThreadLocal來擷取到的值卻是不一樣的,這就是ThreadLocal的奇妙之處。結合這這個例子然後再看一遍前面對ThreadLocal的兩個使用情境的理論分析,大家應該就能比較好地理解ThreadLocal的使用方法了。ThreadLocal之所以有這麼奇妙的效果,是因為不同線程訪問同一個ThreadLocal的get方法,ThreadLocal內部會從各自的線程中取出一個數組,然後再從數組中根據當前ThreadLocal的索引去尋找出對應的value值,很顯然,不同線程中的數組是不同的,這就是為什麼通過ThreadLocal可以在不同的線程中維護一套資料的副本並且彼此互不干擾。

對ThreadLocal的使用方法和工作過程做了一個介紹後,下面分析下ThreadLocal的內部實現, ThreadLocal是一個泛型類,它的定義為public class ThreadLocal,只要弄清楚ThreadLocal的get和set方法就可以明白它的工作原理。

首先看ThreadLocal的set方法,如下所示:

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的資料,如下所示:ThreadLocal.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 = -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;}}}

上面的代碼實現資料的預存程序,這裡不去分析它的具體演算法,但是我們可以得出一個儲存規則,那就是ThreadLocal的值在table數組中的儲存位置總是為ThreadLocal的reference欄位所標識的對象的下一個位置,比如ThreadLocal的reference對象在table數組的索引為index,那麼ThreadLocal的值在table數組中的索引就是index+1。最終ThreadLocal的值將會被儲存在table數組中:table[index + 1] = value。

上面分析了ThreadLocal的set方法,這裡分析下它的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方法的邏輯也比較清晰,它同樣是取出當前線程的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可以在多個線程中互不干擾地儲存和修改資料,理解ThreadLocal的實現方式有助於理解Looper的工作原理。

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.