天天看點

帶你了解源碼中的 ThreadLocal

本篇文章已授權微信公衆号 guolin_blog (郭霖)獨家釋出

這次想來講講 ThreadLocal 這個很神奇的東西,最開始接觸到這個是看了主席的《開發藝術探索》,後來是在研究 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()

方法裡都是調用已經分析過的方法了,那麼就不繼續分析了,注釋裡也寫得很詳細了。

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

Looper.myLooper()

A:因為

Looper.myLooper()

内部其實是調用了 ThreadLocal 的

get()

方法,ThreadLocal 内部會自己去擷取目前線程的成員變量 threadLocals,該變量作用是線程自己的資料存儲容器,作用域自然也就僅限線程而已,以此來實作可以自動根據不同線程傳回各自線程的 Looper 對象。

畢竟,資料本來就隻是存在各自線程中,自然互不影響,ThreadLocal 隻是内部自動先去擷取目前線程對象,再去取對象的資料存儲容器,最後取值傳回而已。

但取值之前要先存值,而在 Looper 類中,對 ThreadLocal 的

set()

方法調用隻有一個地方:

prepare()

,該方法隻有主線程系統已經幫忙調用了。這其實也就是說,主線程的 Looper 消息循環機制是預設開啟的,其他線程預設關閉,如果想要使用,則需要自己手動調用,不調用的話,線程的 Looper 對象一直為空。

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
    protected Choreographer initialValue() {
   	 	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
比如對應 Handler 來說,它需要擷取目前線程的 Looper,很顯然 Looper 的作用域就是線程并且不同線程具有不同的 Looper,這個時候通過 ThreadLocal 就可以輕松實作 Looper 線上程中的存取。如果不采用 ThreadLocal,那麼系統就必須提供一個全局的哈希表供 Handler 查找指定線程的 Looper,這樣一來就必須提供一個類似于 LooperManager 的類了,但是系統并沒有這麼做而是選擇了 ThreadLocal,這就是 ThreadLocal 的好處
  • 場景2

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

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

大家好,我是 dasu,歡迎關注我的公衆号(dasuAndroidTv),如果你覺得本篇内容有幫助到你,可以轉載但記得要關注,要标明原文哦,謝謝支援~