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

發表於2015-10-23

提到訊息機制大家應該都不陌生,在日常開發中不可避免地要涉及到這方面的內容。從開發的角度來說,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型別的,如下所示:

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

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

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

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

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

在上面的set方法中,首先會通過values方法來獲取當前執行緒中的ThreadLocal資料,如果獲取呢?其實獲取的方式也是很簡單的,在Thread類的內容有一個成員專門用於儲存執行緒的ThreadLocal的資料,如下所示:ThreadLocal.Values localValues,因此獲取當前執行緒的ThreadLocal資料就變得異常簡單了。如果localValues的值為null,那麼就需要對其進行初始化,初始化後再將ThreadLocal的值進行儲存。下面看下ThreadLocal的值到底是怎麼localValues中進行儲存的。在localValues內部有一個陣列:private Object[] table,ThreadLocal的值就是存在在這個table陣列中,下面看下localValues是如何使用put方法將ThreadLocal的值儲存到table陣列中的,如下所示:

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

上面分析了ThreadLocal的set方法,這裡分析下它的get方法,如下所示:

       可以發現,ThreadLocal的get方法的邏輯也比較清晰,它同樣是取出當前執行緒的localValues物件,如果這個物件為null那麼就返回初始值,初始值由ThreadLocal的initialValue方法來描述,預設情況下為null,當然也可以重寫這個方法,它的預設實現如下所示:
       如果localValues物件不為null,那就取出它的table陣列並找出ThreadLocal的reference物件在table陣列中的位置,然後table陣列中的下一個位置所儲存的資料就是ThreadLocal的值。

從ThreadLocal的set和get方法可以看出,它們所操作的物件都是當前執行緒的localValues物件的table陣列,因此在不同執行緒中訪問同一個ThreadLocal的set和get方法,它們對ThreadLocal所做的讀寫操作僅限於各自執行緒的內部,這就是為什麼ThreadLocal可以在多個執行緒中互不干擾地儲存和修改資料,理解ThreadLocal的實現方式有助於理解Looper的工作原理。

相關文章