Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android編程入門 >> Android的消息機制之ThreadLocal的工作原理

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

編輯:Android編程入門

 提到消息機制大家應該都不陌生,在日常開發中不可避免地要涉及到這方面的內容。從開發的角度來說,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 ThreadLocal<Boolean>mBooleanThreadLocal = new ThreadLocal<Boolean>();

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

[java] view plain copy  
  1. mBooleanThreadLocal.set(true);  
  2. Log.d(TAG, "[Thread#main]mBooleanThreadLocal=" + mBooleanThreadLocal.get());  
  3.   
  4. new Thread("Thread#1") {  
  5.     @Override  
  6.     public void run() {  
  7.         mBooleanThreadLocal.set(false);  
  8.         Log.d(TAG, "[Thread#1]mBooleanThreadLocal=" + mBooleanThreadLocal.get());  
  9.     };  
  10. }.start();  
  11.   
  12. new Thread("Thread#2") {  
  13.     @Override  
  14.     public void run() {  
  15.         Log.d(TAG, "[Thread#2]mBooleanThreadLocal=" + mBooleanThreadLocal.get());  
  16.     };  
  17. }.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<T>,只要弄清楚ThreadLocal的get和set方法就可以明白它的工作原理。

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

[java] view plain copy  
  1. public void set(T value) {  
  2.     Thread currentThread = Thread.currentThread();  
  3.     Values values = values(currentThread);  
  4.     if (values == null) {  
  5.         values = initializeValues(currentThread);  
  6.     }  
  7.     values.put(this, value);  
  8. }  

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

[java] view plain copy  
  1. void put(ThreadLocal<?> key, Object value) {  
  2.     cleanUp();  
  3.   
  4.     // Keep track of first tombstone. That's where we want to go back  
  5.     // and add an entry if necessary.  
  6.     int firstTombstone = -1;  
  7.   
  8.     for (int index = key.hash & mask;; index = next(index)) {  
  9.         Object k = table[index];  
  10.   
  11.         if (k == key.reference) {  
  12.             // Replace existing entry.  
  13.             table[index + 1] = value;  
  14.             return;  
  15.         }  
  16.   
  17.         if (k == null) {  
  18.             if (firstTombstone == -1) {  
  19.                 // Fill in null slot.  
  20.                 table[index] = key.reference;  
  21.                 table[index + 1] = value;  
  22.                 size++;  
  23.                 return;  
  24.             }  
  25.   
  26.             // Go back and replace first tombstone.  
  27.             table[firstTombstone] = key.reference;  
  28.             table[firstTombstone + 1] = value;  
  29.             tombstones--;  
  30.             size++;  
  31.             return;  
  32.         }  
  33.   
  34.         // Remember first tombstone.  
  35.         if (firstTombstone == -1 && k == TOMBSTONE) {  
  36.             firstTombstone = index;  
  37.         }  
  38.     }  
  39. }  

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

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

[java] view plain copy  
  1. public T get() {  
  2.     // Optimized for the fast path.  
  3.     Thread currentThread = Thread.currentThread();  
  4.     Values values = values(currentThread);  
  5.     if (values != null) {  
  6.         Object[] table = values.table;  
  7.         int index = hash & values.mask;  
  8.         if (this.reference == table[index]) {  
  9.             return (T) table[index + 1];  
  10.         }  
  11.     } else {  
  12.         values = initializeValues(currentThread);  
  13.     }  
  14.   
  15.     return (T) values.getAfterMiss(this);  
  16. }  

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

[java] view plain copy  
  1. /** 
  2.  * Provides the initial value of this variable for the current thread. 
  3.  * The default implementation returns {@code null}. 
  4.  * 
  5.  * @return the initial value of the variable. 
  6.  */  
  7. protected T initialValue() {  
  8.     return null;  
  9. }  

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

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

   
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved