1. 程式人生 > >帶你了解幸運28源碼下載源碼中的 ThreadLocal

帶你了解幸運28源碼下載源碼中的 ThreadLocal

lmap 基於 traversal 希望 不清楚 ofo 運行原理 驗證方式 .com

這次想來講講 ThreadLocal 這個很神奇的東西幸運28源碼下載 QQ295 277 7280【話仙源碼論壇】hxforum.com【木瓜源碼論壇】papayabbs.com,最開始接觸到這個是看了主席的《開發藝術探索》,後來是在研究 ViewRootImpl 中又碰到一次,而且還發現 Android 中一個小彩蛋,就越發覺得這個東西很有趣,那麽便借助主席的這次作業來好好梳理下吧。

提問
開始看源碼前,還是照例來思考一些問題,帶著疑問過源碼比較有條理,效率比較高一點。

大夥都清楚,Android 其實是基於消息驅動機制運行的,主線程有個消息隊列,通過主線程對應的 Looper 一直在對這個消息隊列進行輪詢操作。

但其實,每個線程都可以有自己的消息隊列,都可以有自己的 Looper 來輪詢隊列,不清楚大夥有接觸過 HandlerThread 這東西麽,之前看過一篇文章,通過 HandlerThread 這種單線程消息機制來替代線程同步操作的場景,這種思路很讓人眼前一亮。

而 Looper 有一個靜態方法:Looper.myLooper()

通過這個方法可以獲取到當前線程的 Looper 對象,那麽問題來了:

Q1:在不同線程中調用 Looper.myLooper() 為什麽可以返回各自線程的 Looper 對象呢?明明我們沒有傳入任何線程信息,內部是如何找到當前線程對應的 Looper 對象呢?

我們再來看一段《開發藝術探索》書中的描述:

ThreadLocal 是一個線程內部的數據存儲類,通過它可以在指定的線程中存儲數據,數據存儲以後,只有在指定線程中可以獲取到存儲的數據,對於其他線程來說則無法獲取到數據。

雖然在不同線程中訪問的是同一個 ThreadLocal 對象,但是它們通過 ThreadLocal 獲取到的值卻是不一樣的。

一般來說,當某些數據是以線程為作用域並且不同線程具有不同的數據副本的時候,就可以考慮采用 ThreadLocal。

好,問題來了:

Q2:ThreadLocal 是如何做到同一個對象,卻維護著不同線程的數據副本呢?

源碼分析
ps:ThreadLocal 內部實現在源碼版本 android-24 做了改動,《開發藝術探索》書中分析的源碼是 android-24 版本之前的實現原理,本篇分析的源碼版本基於 android-25,感興趣的可以閱讀完本篇再去看看《開發藝術探索》,比較一下改動前後的實現原理是否有何不同。

因為是從 Q1 深入才接觸到 ThreadLocal 的,那麽這次源碼閱讀的入口很簡單,也就是 Looper.myLopper():

//Looper#myLooper()
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
所以,Looper.myLooper() 實際上是調用的 ThreadLocal 的 get() 方法,也就是說,Looper.myLooper() 能實現即使不傳入線程信息也能獲取到各自線程的 Looper 是通過 ThreadLocal 實現的。

get()
那麽,下面就繼續跟著走下去吧:

//ThreadLocal#get()
public T get() {
//1. 獲取當前的線程
Thread t = Thread.currentThread();
//2. 以當前線程為參數,獲取一個 ThreadLocalMap 對象
ThreadLocalMap map = getMap(t);
if (map != null) {
//3. map 不為空,則以當前 ThreadLocal 對象實例作為key值,去map中取值,有找到直接返回
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
//4. map 為空或者在map中取不到值,那麽走這裏,返回默認初始值
return setInitialValue();
}
所有的關鍵點就是從這裏開始看了,到底 ThreadLocal 是如何實現即使調用同一個對象同一個方法,卻能自動根據當前線程返回不同的數據,一步步來看。

首先,獲取當前線程對象。

接著,調用了 getMap() 方法,並傳入了當前線程,看看這個 getMap() 方法:

//ThreadLocal#getMap()
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
原來直接返回線程的 threadLocals 成員變量,由於 ThreadLocal 與 Thread 位於同一個包中,所以可以直接訪問包權限的成員變量。我們接著看看 Thread 中的這個成員變量 threadLocals :

//Thread.threadLocal
ThreadLocal.ThreadLocalMap threadLocals = null;

//ThreadLocal#createMap()
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
Thread 中的 threadLocal 成員變量初始值為 null,並且在 Thread 類中沒有任何賦值的地方,只有在 ThreadLocal 中的 createMap() 方法中對其賦值,而調用 createMap() 的地方就兩個:set() 和 setInitialValue(),而調用 setInitialValue() 方法的地方只有 get()。

也就是說,ThreadLocal 的核心其實也就是在 get() 和 set(),搞懂這兩個方法的流程原理,那麽也就基本理解 ThreadLocal 這個東西的原理了。

到這裏,先暫時停一停,我們先來梳理一下目前的信息,因為到這裏為止應該對 ThreadLocal 原理有點兒眉目了:

不同線程調用相同的 Looper.myLooper(),其實內部是調用了 ThreadLocal 的 get() 方法,而 get() 方法則在一開始就先獲取當前線程的對象,然後直接通過包權限獲取當前線程的 threadLocals 成員變量,該變量是一個 ThreadLocal 的內部類 ThreadLocalMap 對象,初始值為 null。

以上,是我們到目前所梳理的信息,雖然我們還不知道 ThreadLocalMap 作用是什麽,但不妨礙我們對其進行猜測啊。如果這個類是用於存儲數據的,那麽一切是不是就可以說通了!

為什麽不同線程中明明調用了同一對象的同一方法,卻可以返回各自線程對應的數據呢?原來,這些數據本來就是存儲在各自線程中了,ThreadLocal 的 get() 方法內部其實會先去獲取當前的線程對象,然後直接將線程存儲的容器取出來。

所以,我們來驗證一下,ThreadLocalMap 是不是一個用於存儲數據的容器類:

//ThreadLocal$ThreadLocalMap
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal> {
Object value;
}
private Entry[] table;

private void set(ThreadLocal key, Object value) {
    ...
}

private Entry getEntry(ThreadLocal key) {
    ...   
}

}
猜對了,很明顯,ThreadLocalMap 就是一個用於存儲數據的容器類,set 操作,get 操作,連同容器數組都有了,這樣一個類不是用於存儲數據的容器類還是什麽。至於它內部的各種擴容算法,hash 算法,我們就不管了,不深入下去了,知道這個類是幹嘛用的就夠了。當然,感興趣你可以自行深入研究。

那麽,好,我們回到最初的 ThreadLocal 的 get() 方法中繼續分析:

//ThreadLocal#get()
public T get() {
//1. 獲取當前的線程
Thread t = Thread.currentThread();
//2. 以當前線程為參數,獲取一個 ThreadLocalMap 對象
ThreadLocalMap map = getMap(t);
if (map != null) {
//3. map 不為空,則以當前 ThreadLocal 對象實例作為key值,去map中取值,有找到直接返回
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
//4. map 為空或者在map中取不到值,那麽走這裏,返回默認初始值
return setInitialValue();
}
第 1 步,第 2 步我們已經梳理清楚了,就是去獲取當前線程的數據存儲容器,也就是 map。拿到容器之後,其實也就分了兩條分支走,一是容器不為 null,一是容器為 null 的場景。我們先來看看容器為 null 場景的處理:

//ThreadLocal#setInitialValue()
private T setInitialValue() {
//1. 獲取初始值,默認返回Null,允許重寫
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//2. 創建線程t的數據存儲容器:threadLocals
createMap(t, value);
//3. 返回初始值
return value;
}
首先會通過 initialValue() 去獲取初始值,默認實現是返回 null,但該方法允許重寫。然後同樣去獲取當前線程的數據存儲容器 map,為null,所以這裏會走 createMap(),而 createMap() 我們之前分析過了,就是去創建參數傳進去的線程自己的數據存儲容器 threadLocals,並將初始值保存在容器中,最後返回這個初始值。

那麽,這條分支到這裏就算結束了,我們回過頭繼續看另一條分支,都跟完了再來小結:

//ThreadLocal#get()
public T get() {
//1. 獲取當前的線程
Thread t = Thread.currentThread();
//2. 以當前線程為參數,獲取一個 ThreadLocalMap 對象
ThreadLocalMap map = getMap(t);
if (map != null) {
//3. map 不為空,則以當前 ThreadLocal 對象實例作為key值,去map中取值,有找到直接返回
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
//4. map 為空或者在map中取不到值,那麽走這裏,返回默認初始值
return setInitialValue();
}
另一條分支很簡單,也就是如果線程的數據存儲容器不為空,那麽就以當前 ThreadLocal 對象實例作為 key 值,去這個容器中尋找對應的數據,如果有找到直接返回,沒找到,那麽就走 setInitialValue(),該方法內部會去取默認的初始值,然後以當前 ThreadLocal 對象實例作為 key 值存入到當前線程的數據存儲容器中,並返回初始值。

到這裏,get() 的流程已經梳理完畢了,那就先來小結一下:

當不同的線程調用同一個 ThreadLocal 對象的 get() 方法時,內部其實是會先獲取當前線程的對象,然後通過包權限直接獲取對象的數據存儲容器 ThreadLocalMap 對象,如果容器為空,那麽會新建個容器,並將初始值和當前 ThreadLocal 對象綁定存儲進去,同時返回初始值;如果容器不為空,那麽會以當前 ThreadLocal 對象作為 key 值去容器中尋找,有找到直接返回,沒找到,那麽以同樣的操作先存入容器再返回初始值。

這種設計思想很巧妙,首先,容器是各自線程對象的成員變量,也就是數據其實就是交由各自線程維護,那麽不同線程即使調用了同一 ThreadLocal 對象的同一方法,取的數據也是各自線程的數據副本,這樣自然就可以達到維護不同線程各自相互獨立的數據副本,且以線程為作用域的效果了。

同時,在將數據存儲到各自容器中是以當前 ThreadLocal 對象實例為 key 存儲,這樣,即使在同一線程中調用了不同的 ThreadLocal 對象的 get() 方法,所獲取到的數據也是不同的,達到同一線程中不同 ThreadLocal 雖然共用一個容器,但卻可以相互獨立運作的效果。

(特別佩服 Google 工程師!)

set()
get() 方法我們已經梳理完了,其實到這裏,ThreadLocal 的原理基本上算是理清了,而且有一點,梳理到現在,其實 ThreadLocal 該如何使用我們也可以猜測出來了。

你問我為什麽可以猜測出來了?

忘了我們上面梳理的 get() 方法了麽,內部會一直先去取線程的容器,然後再從容器中取最後的值,取不到就會一直返回初始值,會有哪種應用場景是需要一直返回初始值的麽?肯定沒有,既然如此,就要保證在容器中可以取到值,那麽,自然就是要先 set() 將數據存到容器中,get() 的時候才會有值啊。

所以,用法很簡單,實例化 ThreadLocal 對象後,直接調用 set() 存值,調用 get() 取值,兩個方法內部會自動根據當前線程選擇相對應的容器存取。

我們來看看 set() 是不是這樣:

//ThreadLocal#set()
public void set(T value) {
//1. 取當前線程對象
Thread t = Thread.currentThread();
//2. 取當前線程的數據存儲容器
ThreadLocalMap map = getMap(t);
if (map != null)
//3. 以當前ThreadLocal實例對象為key,存值
map.set(this, value);
else
//4. 新建個當前線程的數據存儲容器,並以當前ThreadLocal實例對象為key,存值
createMap(t, value);
}
是吧,set() 方法裏都是調用已經分析過的方法了,那麽就不繼續分析了,註釋裏也寫得很詳細了。

那麽,最後來回答下開頭的兩個問題:

Q1:在不同線程中調用 Looper.myLooper() 為什麽可以返回各自線程的 Looper 對象呢?明明我們沒有傳入任何線程信息,內部是如何找到當前線程對應的 Looper 對象呢?

A:因為 Looper.myLooper() 內部其實是調用了 ThreadLocal 的 get() 方法,ThreadLocal 內部會自己去獲取當前線程的成員變量 threadLocals,該變量作用是線程自己的數據存儲容器,作用域自然也就僅限線程而已,以此來實現可以自動根據不同線程返回各自線程的 Looper 對象。

畢竟,數據本來就只是存在各自線程中,自然互不影響,ThreadLocal 只是內部自動先去獲取當前線程對象,再去取對象的數據存儲容器,最後取值返回而已。

但取值之前要先存值,而在 Looper 類中,對 ThreadLocal 的 set() 方法調用只有一個地方: prepare(),該方法只有主線程系統已經幫忙調用了。這其實也就是說,主線程的 Looper 消息循環機制是默認開啟的,其他線程默認關閉,如果想要使用,則需要自己手動調用,不調用的話,線程的 Looper 對象一直為空。

Q2:ThreadLocal 是如何做到同一個對象,卻維護著不同線程的數據副本呢?

A:梳理清楚,其實好像也不是很難,是吧。無外乎就是將數據保存在各自的線程中,這樣不同線程的數據自然相互不影響。然後存值時再以當前 ThreadLocal 實例對象為 key,這樣即使同一線程中,不同 ThreadLocal 雖然使用同一個容器,但 key 不一樣,取值時也就不會相互影響。

小彩蛋
說是小彩蛋,其實是 Android 的一個小 bug,盡管這個 bug 並不會有任何影響,但發現了 Google 工程師居然也寫了 bug,就異常的興奮有沒有。

另外,先說明下,該 bug 並不是我發現的,我以前在寫一篇博客分析 View.post 源碼時,期間有個問題卡住,然後閱讀其他大神的文章時發現他提了這點,bug 是他發現並不是由我發現,只是剛好,我看的源碼版本比他的新,然後發現在我看的源碼版本上,這個 bug 居然被修復了,那麽也就是說, Google 的這一點行為也就表示這確實是一個 bug,所以異常興奮,特別佩服那個大神。

是這樣的,不清楚 View.post() 流程原理的可以先去我那篇博客過過,不過也麽事,我簡單來說下:

通過 View.post(Runnable action) 傳進來的 Runnable,如果此時 View 還沒 attachToWindow,那麽這個 Runnable 是會先被緩存起來,直到 View 被 attachToWindow 時才取出來執行。

而在版本 android-24 之前,緩存是交由 ViewRootImpl 來做的,如下:

//View#post()
public boolean post(Runnable action) {
//1. mAttachInfo 是當 View 被 attachToWindow 時才會被賦值
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
//2. 所以,如果 View 還沒被 attachToWindow 時,這些 Runnable 會先被緩存起來
ViewRootImpl.getRunQueue().post(action);
return true;
}
mAttachInfo 是當 View 被 attachToWindow 時才會被賦值,所以,如果 View 還沒被 attachToWindow 時,這些 Runnable 會先被緩存起來,版本 android-24 之前的實現是交由 ViewRootImpl 實現,如下:

//ViewRootImpl#getRunQueue()
static RunQueue getRunQueue() {
RunQueue rq = sRunQueues.get();
if (rq != null) {
return rq;
}
rq = new RunQueue();
sRunQueues.set(rq);
return rq;
}

//ViewRootImpl.sRunQueues
static final ThreadLocal<RunQueue> sRunQueues = new ThreadLocal<RunQueue>();
這點關鍵點是,sRunQueues 是一個 ThreadLocal 對象,而且我們使用 View.post() 是經常有可能在各種子線程中的,為的就是利用這個方法方便的將 Runnable 切到主線程中執行,但這樣的話,其實如果在 View 還沒被 attachToWindow 時,這些 Runnable 就是被緩存到各自線程中了,因為使用的是 ThreadLocal。

而這些被緩存起來的 Runnable 被取出來執行的地方是在 ViewRootImpl 的 performTraversals(),這方法是控制 View 樹三大流程:測量、布局、繪制的發起者,而且可以肯定的是,這方法肯定是運行在主線程中的。

那麽,根據我們分析的 ThreadLocal 原理,不同線程調用 get() 方法時數據是相互獨立的,存值的時候有可能是在各種線程中,所以 Runnable 被緩存到各自的線程中去,但取值執行時卻只在主線程中取,這樣一來,就會造成很多緩存在其他子線程中的 Runnable 就被丟失掉了,因為取不到,自然就執行不了了。

驗證方式也很簡單,切到 android-24 之前的版本,然後隨便在 Activity 的 onCreate() 裏寫段在子線程中調用 View.post(Runnable),看看這個 Runnable 會不會被執行就清楚了。

更具體的分析看那個大神的博客:通過View.post()獲取View的寬高引發的兩個問題

而在 android-24 版本之後,源碼將這個實現改掉了,不用 ThreadLocal 來做緩存了,而是直接讓各自的 View 內部去維護了,具體不展開了,感興趣可以去看看我那篇博客和那個大神的博客。

PS:另外,不知道大夥註意到了沒有,android-24 版本的源碼是不是發生了什麽大事,在這個版本好像改動了很多原本內部的實現,比如一開頭分析的 ThreadLocal 內部實現在這個版本也改動了,上面看的 View.post() 在這個版本也改動了。

應用場景
源碼中的應用場景
源碼內部很多地方都有 ThreadLocal 的身影,其實這也說明了在一些場景下,使用 ThreadLocal 是可以非常方便的幫忙解決一些問題,但如果使用不當的話,可能會造成一些問題,就像上面說過的在 android-24 版本之前 View.post() 內部采用 ThreadLocal 來做緩存,如果考慮不當,可能會造成丟失一些緩存的問題。

場景1:Looper.myLooper()
用於不用線程獲取各自的 Looper 的需求,具體見上文。

場景2:View.post()
android-24 版本之前用於緩存 Runnable,具體見上文。

場景3:AnimationHandler
大夥不清楚對這個熟悉不,我之前寫過一篇分析 ValueAnimator 運行原理,所以有接觸到這個。先看一下,它內部是如何使用 ThreadLocal 的:

//AnimationHandler.sAnimatorHandler
public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();

//AnimationHandler#getInstance()
public static AnimationHandler getInstance() {
if (sAnimatorHandler.get() == null) {
sAnimatorHandler.set(new AnimationHandler());
}
return sAnimatorHandler.get();
}
單例 + ThreadLocal? 是不是突然又感覺眼前一亮,居然可以這麽用!

那麽這種應用場景是什麽呢,首先,單例,那麽就說明只存在一個實例,希望外部只使用這麽一個實例對象。然後,單例又結合了 ThreadLocal,也就是說,希望在同一個線程中實例對象只有一個,但允許不同線程有各自的單例實例對象。

而源碼這裏為什麽需要這麽使用呢,我想了下,覺得應該是這樣的,個人觀點,還沒理清楚,不保證完全正確,僅供參考:

動畫的實現肯定是需要監聽 Choreographer 的每一幀 vsync 信息事件的,那麽在哪裏發起監聽,在哪裏接收回調,屬性動畫就則是通過一個單例類 AnimationHandler 來實現。也就是,程序中,所有的屬性動畫共用一個 AnimationHandler 單例來監聽 Choreographer 的每一幀 vsync 信號事件。

那麽 AnimationHandler 何時決定不監聽了呢?不是某個動畫執行結束就取消監聽,而是所有的動畫都執行完畢,才不會再發起監聽,那麽,它內部其實就維護著所有正在運行中的動畫信息。所以,在一個線程中它必須也只能是單例模式。

但是,ValueAnimator 其實不僅僅可以用來實現動畫,也可以用來實現一些跟幀率相關的業務場景,也就是說,如果不涉及 ui 的話,也是允許在其他子線程中使用 ValueAnimator 的,那麽此時,這些工作就不應該影響到主線程的動畫,那麽它是需要單獨另外一份 AnimationHandler 單例對象來管理了。

兩者結合下,當有在線程內需要單例模式,而又允許不同線程相互獨立運作的場景時,也可以使用 ThreadLocal。

場景4:Choreographer
//Choreographer.sThreadInstance
private static final ThreadLocal<Choreographer> sThreadInstance = new ThreadLocal<Choreographer>() {@Override
br/>@Override
Looper looper = Looper.myLooper();
if (looper == null) {
throw new IllegalStateException("The current thread must have a looper!");
}
return new Choreographer(looper);
}
}
//Choreographer#getInstance()
public static Choreographer getInstance() {
return sThreadInstance.get();
}
Choreographer 在 Android 的屏幕刷新機制中扮演著非常重要的角色,想了解的可以看看我之前寫的一篇文章:Android 屏幕刷新機制

具體也就不分析了,在這裏也列出這個,只是想告訴大夥,在源碼中,單例 + ThreadLocal 這種模式蠻常見的,我們有要求線程安全的單例模式,相對應的自然也會有線程內的單例模式,要求不同線程可以互不影響、獨立運作的單例場景,如果大夥以後有遇到,不妨嘗試就用 ThreadLocal 來實現看看。

其他
源碼中,還有很多地方也有用到,View 中也有,ActivityThread 也有,ActivityManagerService 也有,很多很多,但很多地方的應用場景我也還搞不懂,所以也就不列舉了。總之,就像主席在《開發藝術探索》中所說的:

一般來說,當某些數據是以線程為作用域並且不同線程具有不同的數據副本的時候,就可以考慮采用 ThreadLocal

精辟,上述源碼中不管是用於緩存功能,還是要求線程獨立,還是單例 + ThreadLocal 模式,其實本質上都是上面那句話:某些數據如果是以線程為作用域並且不同線程可以互不影響、獨立運作的時候,那麽就可以采用 ThreadLocal 了。

《開發藝術探索》中描述的應用場景
場景1
一般來說,當某些數據是以線程為作用域並且不同線程具有不同的數據副本的時候,就可以考慮采用 ThreadLocal。

比如對應 Handler 來說,它需要獲取當前線程的 Looper,很顯然 Looper 的作用域就是線程並且不同線程具有不同的 Looper,這個時候通過 ThreadLocal 就可以輕松實現 Looper 在線程中的存取。如果不采用 ThreadLocal,那麽系統就必須提供一個全局的哈希表供 Handler 查找指定線程的 Looper,這樣一來就必須提供一個類似於 LooperManager 的類了,但是系統並沒有這麽做而是選擇了 ThreadLocal,這就是 ThreadLocal 的好處

場景2
ThreadLocal 另一個使用場景是復雜邏輯下的對象傳遞,比如監聽器的傳遞,有些時候一個線程中的任務過於復雜,這可能表現為函數調用棧比較深以及代碼入口多樣性,在這種情況下,我們又需要監聽器能夠貫穿整個線程的執行過程,這個時候可以怎麽做呢?

其實這時就可以采用 ThreadLocal,采用 ThreadLocal 可以讓監聽器作為線程內的全局對象而存在,在線程內部只要通過 get 方法就可以獲取到監聽器。如果不采用 ThreadLocal,那麽我們能想到的可能是如下兩種方法:第一種方法是將監聽器通過參數的形式在函數調用棧中進行傳遞,第二種方法就是將監聽器作為靜態變量供線程訪問。上述這兩種方法都是有局限性的。第一種方法的問題是當函數調用棧很深的時候,通過函數參數來傳遞監聽器對象這幾乎是不可接受的,這會讓程序的設計看起來糟糕。第二種方法是可以接受的,但是這種狀態是不具有可擴充性的,比如同時有兩個線程在執行,那麽就需要提供兩個靜態的監聽器對象,如果有 10 個線程在並發執行呢?提供 10 個靜態的監聽器對象?這顯然是不可思議的,而采用 ThreadLocal,每個監聽器對象都在自己的線程內部存儲,根本就不會有方法 2 的這種問題。

帶你了解幸運28源碼下載源碼中的 ThreadLocal