1. 程式人生 > >[轉]Android的ThreadLocal的工作原理

[轉]Android的ThreadLocal的工作原理

Looper中有一個特殊的概念,那就是ThreadLocalThreadLocal並不是執行緒,它的作用是可以在每個執行緒中儲存資料。大家知道,Handler建立的時候會採用當前執行緒的Looper來構造訊息迴圈系統,那麼Handler內部如何獲取到當前執行緒的Looper呢?這就要使用ThreadLocal了,ThreadLocal可以在不同的執行緒之中互不干擾地儲存並提供資料。

ThreadLocal是一個執行緒內部的資料儲存類,通過它可以在指定的執行緒中儲存資料,資料儲存以後,只有在指定執行緒中可以獲取到儲存的資料,對於其它執行緒來說無法獲取到資料。在日常開發中用到ThreadLocal

的地方較少,但是在某些特殊的場景下,通過ThreadLocal可以輕鬆地實現一些看起來很複雜的功能,這一點在Android的原始碼中也有所體現,比如LooperActivityThread以及AMS中都用到了ThreadLocal。具體到ThreadLocal的使用場景,這個不好統一地來描述,一般來說,當某些資料是以執行緒為作用域並且不同執行緒具有不同的資料副本的時候,就可以考慮採用ThreadLocal。比如對於Handler來說,它需要獲取當前執行緒的Looper,很顯然Looper的作用域就是執行緒並且不同執行緒具有不同的Looper,這個時候通過ThreadLocal就可以輕鬆實現Looper
線上程中的存取,如果不採用ThreadLocal,那麼系統就必須提供一個全域性的雜湊表供Handler查詢指定執行緒的Looper,這樣一來就必須提供一個類似於LooperManager的類了,但是系統並沒有這麼做而是選擇了ThreadLocal,這就是ThreadLocal的好處。

ThreadLocal另一個使用場景是複雜邏輯下的物件傳遞,比如監聽器的傳遞,有些時候一個執行緒中的任務過於複雜,這可能表現為函式呼叫棧比較深以及程式碼入口的多樣性,在這種情況下,我們又需要監聽器能夠貫穿整個執行緒的執行過程,這個時候可以怎麼做呢?其實就可以採用ThreadLocal,採用ThreadLocal

可以讓監聽器作為執行緒內的全域性物件而存在,線上程內部只要通過get方法就可以獲取到監聽器。

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

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的值,根據前面對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之所以有這麼奇妙的效果,是因為不同執行緒訪問同一個ThreadLocalget方法,ThreadLocal內部會從各自的執行緒中取出一個數組,然後再從陣列中根據當前ThreadLocal的索引去查找出對應的value值,很顯然,不同執行緒中的陣列是不同的,這就是為什麼通過ThreadLocal可以在不同的執行緒中維護一套資料的副本並且彼此互不干擾。

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

首先看ThreadLocalset方法,如下所示:

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[] tableThreadLocal的值就是存在在這個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陣列中的儲存位置總是為ThreadLocalreference欄位所標識的物件的下一個位置,比如ThreadLocalreference物件在table陣列的索引為index,那麼ThreadLocal的值在table陣列中的索引就是index+1。最終ThreadLocal的值將會被儲存在table陣列中:table[index + 1] = value。

上面分析了ThreadLocalset方法,這裡分析下它的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);
}

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

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

程式碼分析基於L

來張L上的ThreadLocal圖:
在這裡插入圖片描述

在Android7.0之後ThreadLocal的實現跟JDK7上保持了一致,來張JDK7上的ThreadLocal圖吧
在這裡插入圖片描述

個人感覺這樣的修改,跟那個WeakReference導致的GC有關,以後再來求證下吧。[TODO]

參考文章:
https://blog.csdn.net/singwhatiwanna/article/details/48350919