1. 程式人生 > >Android關於ThreadLocal的思考和總結

Android關於ThreadLocal的思考和總結

前言

Handler機制引出ThreadLocal

  • 關於ThreadLocal的分析,首先得從Android的訊息機制談起,可能我們最先想到的就是Android訊息機制的上層介面Handler
  • 為了避免ANR,我們會通常把耗時操作放在子執行緒裡面去執行,因為子執行緒不能更新UI,所以當子執行緒需要更新UI的時候就需要藉助到Android的訊息機制,也就是Handler機制了

關於Handler的原理,不是本文剖析的重點,這裡僅給出一些相關結論,同時引出今天的主角ThreadLocal

  • Handler的處理過程執行在建立Handler的執行緒裡
  • 一個Looper對應一個MessageQueue
  • 一個執行緒對應一個Looper
  • 一個Looper可以對應多個Handler
  • 執行緒是預設沒有Looper的,執行緒需要通過Looper.prepare()、繫結Handler到Looper物件、Looper.loop()來建立訊息迴圈
  • 主執行緒(UI執行緒),也就是ActivityThread,在被建立的時候就會初始化Looper,所以主執行緒中可以預設使用Handler
  • 可以通過Looper的quitSafely()或者quit()方法終結訊息迴圈,quitSafely相比於quit方法安全之處在於清空訊息之前會派發所有的非延遲訊息。
  • 不確定當前執行緒時,更新UI時儘量呼叫post方法

如何保證一個執行緒對應一個Looper,同時各個執行緒之間的Looper互不干擾就引出了接下來要討論的ThreadLocal

 public final class Looper {
    private static final String TAG = "Looper";
    // sThreadLocal.get() will return null unless you've called prepare().
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

    ....//省略
}

分析

案例展示及執行結果

這裡先給出ThreadLocal和InheritableThreadLocal的簡單實用demo

public class ThreadLocalTest {
    static final String CONSTANT_01 = "CONSTANT_01";
    static final String CONSTANT_02 = "CONSTANT_02";

    public static void main(String[] args) throws InterruptedException {
        ThreadLocal<String> threadLocal = new ThreadLocal<String>();
        threadLocal.set(CONSTANT_01);

        InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<String>();
        inheritableThreadLocal.set(CONSTANT_01);

        Thread thread_1 = new TestThread(threadLocal, inheritableThreadLocal);
        thread_1.setName("thread_01");
        thread_1.start();

        thread_1.join();

        System.out.println("   " + Thread.currentThread().getName() + "  ******************************************");
        System.out.println("   " + Thread.currentThread().getName() + "   \tThreadLocal: " + threadLocal.get());
        System.out.println("   " + Thread.currentThread().getName() + "   \tInheritableThreadLocal: " + inheritableThreadLocal.get());
        System.out.println("   " + Thread.currentThread().getName() + "  ******************************************");
    }
}

class TestThread extends Thread {
    ThreadLocal<String> threadLocal;
    InheritableThreadLocal<String> inheritableThreadLocal;

    public TestThread(ThreadLocal<String> threadLocal, InheritableThreadLocal<String> inheritableThreadLocal) {
        super();
        this.threadLocal = threadLocal;
        this.inheritableThreadLocal = inheritableThreadLocal;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + "******************************************");
        System.out.println(Thread.currentThread().getName() + "\tThreadLocal: " + threadLocal.get());
        System.out.println(Thread.currentThread().getName() + "\tInheritableThreadLocal: " + inheritableThreadLocal.get());
        System.out.println(Thread.currentThread().getName() + "******************************************\n");

        threadLocal.set(ThreadLocalTest.CONSTANT_02);
        inheritableThreadLocal.set(ThreadLocalTest.CONSTANT_02);

        System.out.println(Thread.currentThread().getName() + "*************(Reset Value)****************");
        System.out.println(Thread.currentThread().getName() + "\tThreadLocal: " + threadLocal.get());
        System.out.println(Thread.currentThread().getName() + "\tInheritableThreadLocal: " + inheritableThreadLocal.get());
        System.out.println(Thread.currentThread().getName() + "*************(Reset Value)****************\n");
    }
}

執行結果:

thread_01******************************************
thread_01   ThreadLocal: null
thread_01   InheritableThreadLocal: CONSTANT_01
thread_01******************************************

thread_01*************(Reset Value)****************
thread_01   ThreadLocal: CONSTANT_02
thread_01   InheritableThreadLocal: CONSTANT_02
thread_01*************(Reset Value)****************

   main  ******************************************
   main     ThreadLocal: CONSTANT_01
   main     InheritableThreadLocal: CONSTANT_01
   main  ******************************************

如果這個時候你對執行結果有疑問 或者說 「我擦」怎麼又突然冒出來一個InheritableThreadLocal,那麼請繼續往下看

ThreadLocal類結構預覽

當然,我們肯定要先從ThreadLocal開始說起:

先從大體上看一下,可以發現,Java和Android中ThreadLocal的類結構(包括部分細節)還是有一些區別的,不過Android中的實現方式越來越貼近Java版

第一張圖為jdk1.8.0_131中ThreadLocal的類結構:




第二張圖為android-25中ThreadLocal的類結構:

這裡寫圖片描述

ThreadLocal探祕

這裡主要以Android-25(Android7.1.1)的原始碼為基礎進行分析,其實幾乎和Java版本的原始碼一致

首先澄清一下對ThreadLocal的錯誤認知:

  • ThreadLocal為解決多執行緒程式的併發問題提供了一種新的思路
  • ThreadLocal的目的是為了解決多執行緒訪問資源時的共享問題

為什麼這麼說那?
我們看看Android原始碼中是如何介紹ThreadLocal的:

This class provides thread-local variables. These variables differ from
their normal counterparts in that each thread that accesses one (via its
get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).

描述的大致意思是這樣:ThreadLocal類用來提供執行緒內部的區域性變數。這種變數在多執行緒環境下訪問(通過get或set方法訪問)時能保證各個執行緒裡的變數相對獨立於其他執行緒內的變數。ThreadLocal例項通常來說都是private static型別的,用於關聯執行緒和執行緒的上下文。

可以這麼總結:ThreadLocal的作用是提供執行緒內的區域性變數,這種變數線上程的生命週期內起作用,減少同一個執行緒內多個函式或者元件之間一些公共變數的傳遞的複雜度。

有時候大家會拿同步機制(如synchronized)和ThreadLocal做對比,怎麼說才能不引起誤解那?
可以這麼理解:
對於多執行緒資源共享的問題,前者僅提供一份變數,讓不同的執行緒排隊訪問,而後者為每一個執行緒都提供了一份變數,因此可以同時訪問而互不影響。但是ThreadLocal卻並不是為了解決併發或者多執行緒資源共享而設計的

所以ThreadLocal既不是為了解決共享多執行緒的訪問問題,更不是為了解決執行緒同步問題,ThreadLocal的設計初衷就是為了提供執行緒內部的區域性變數,方便在本執行緒內隨時隨地的讀取,並且與其他執行緒隔離。

ThreadLocal的應用場景:

  • 當某些資料是以執行緒為作用域並且不同執行緒具有不同的資料副本的時候
    如:屬性動畫為每個執行緒設定AnimationHandler、Android的Handler訊息機制中通過ThreadLocal實現Looper線上程中的存取、EventBus獲取當前執行緒的PostingThreadState物件或者即將被分發的事件佇列或者當前執行緒是否正在進行事件分發的布林值

  • 複雜邏輯下的物件傳遞
    使用引數傳遞的話:當函式呼叫棧更深時,設計會很糟糕,為每一個執行緒定義一個靜態變數監聽器,如果是多執行緒的話,一個執行緒就需要定義一個靜態變數,無法擴充套件,這時候使用ThreadLocal就可以解決問題。

ThreadLocal原始碼解讀

建構函式:

    public ThreadLocal() {
    }

建立一個執行緒的本地變數

initialValue函式:

    protected T initialValue() {
        return null;
    }

該函式在呼叫get函式的時候會第一次呼叫,但是如果一開始就呼叫了set函式,則該函式不會被呼叫。通常該函式只會被呼叫一次,除非手動呼叫了remove函式之後又呼叫get函式,這種情況下,get函式中還是會呼叫initialValue函式。該函式是protected型別的,很顯然是建議在子類過載該函式的,所以通常該函式都會以匿名內部類的形式被過載,以指定初始值,比如

public class TestThreadLocal {
    private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return Integer.valueOf(1);
        }
    };
}

get函式:

該函式用來獲取與當前執行緒關聯的ThreadLocal的值,如果當前執行緒沒有該ThreadLocal的值,則呼叫initialValue函式獲取初始值返回

   public T get() {
        //1、首先獲取當前執行緒
        Thread t = Thread.currentThread();
        //2、根據當前執行緒獲取一個map
        ThreadLocalMap map = getMap(t);
        //3、如果獲取的map不為空,則在map中以ThreadLocal的引用作為key來在Map中獲取對應的Entry e,否則轉到5
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            //4、如果e不為null,則返回e.value,否則轉到5
            if (e != null)
                return (T)e.value;
        }
        //5、map為空或者e為空,則通過initialValue函式獲取初始值value,然後用ThreadLocal的引用和value作為firstKey和firstValue建立一個新的map
        return setInitialValue();
    }

   ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

   private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
 void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

值得注意的是,上面getMap方法中獲取的threadLocals即是Thread中的一個成員變數

 public class Thread implements Runnable {
    ...//省略
    /* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    /*
     * InheritableThreadLocal values pertaining to this thread. This map is maintained by the InheritableThreadLocal class.*/
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    ...//省略
}

這裡的inheritableThreadLocals會在下文分析InheritableThreadLocal涉及到

set函式:

set函式用來設定當前執行緒的該ThreadLocal的值,設定當前執行緒的ThreadLocal的值為value

    public void set(T value) {
        //1、首先獲取當前執行緒
        Thread t = Thread.currentThread();
        //2、根據當前執行緒獲取一個map
        ThreadLocalMap map = getMap(t);
        if (map != null)
        //3、map不為空,則把鍵值對儲存到map中
            map.set(this, value);
        //4、如果map為空(第一次呼叫的時候map值為null),則去建立一個ThreadLocalMap物件並賦值給map,並把鍵值對儲存到map中。
        else
            createMap(t, value);
    }

remove函式:

remove函式用來將當前執行緒的ThreadLocal繫結的值刪除,在某些情況下需要手動呼叫該函式,防止記憶體洩露。

 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

ThreadLocalMap

可以看成一個HashMap,但是它本身具體的實現卻與java.util.Map沾不上一點關係。只是內部的實現跟HashMap類似(通過雜湊表的方式儲存)。

static class ThreadLocalMap {
    static class Entry extend WeakReference<ThreadLocal> {
    /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
        ...//省略
 }

大致類結構如下圖所示:


這裡寫圖片描述

ThreadLocalMap中定義了Entry陣列例項table,用於儲存Entry。相當於使用一個數組維護一張雜湊表,負載因子是最大容量的2/3
 private Entry[] table;

關於ThreadLocalMap重要函式的分析會結合下一節ThreadLocal記憶體洩漏的問題一併討論

PS:Android早期版本,這部分的資料結構是通過Values實現的,Values中也有一個table的成員變數,table是一個Object陣列,也是以類似map的方式來儲存的。偶數單元儲存的是key,key的下一個單元儲存的是對應的value,所以每儲存一個元素,需要兩個單元,所以容量一定是2的倍數。這裡的key儲存的也是ThreadLocal例項的弱引用

ThreadLocal記憶體洩漏的問題

ThreadLocal裡面使用了一個存在弱引用的map,當釋放掉ThreadLocal的強引用以後,map裡面的value卻沒有被回收.而這塊value永遠不會被訪問到了. 所以存在著記憶體洩露. 最好的做法是將呼叫ThreadLocal的remove方法.

在ThreadLocal的生命週期中,都存在這些引用.

看下圖(來源參考): 實線代表強引用,虛線代表弱引用.


這裡寫圖片描述
  • 每個thread中都存在一個map, map的型別是ThreadLocal.ThreadLocalMap. Map中的key為一個ThreadLocal例項. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每個key都弱引用指向ThreadLocal. 當把ThreadLocal例項置為null以後,沒有任何強引用指向ThreadLocal例項,所以ThreadLocal將會被gc回收. 但是,我們的value卻不能回收,因為存在一條從current thread連線過來的強引用. 只有當前thread結束以後, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將全部被GC回收.
  • 所以得出一個結論就是隻要這個執行緒物件被gc回收,就不會出現記憶體洩露,但在ThreadLocal設為null和執行緒結束這段時間不會被回收的,就發生了我們認為的記憶體洩露。其實這是一個對概念理解的不一致,也沒什麼好爭論的。最要命的是執行緒物件不被回收的情況,這就發生了真正意義上的記憶體洩露。比如使用執行緒池的時候,執行緒結束是不會銷燬的,會再次使用的。就可能出現記憶體洩露。  
  • 為了最小化減少記憶體洩露的可能性和影響,(設計中加上了一些防護措施)在ThreadLocal的get,set的時候都會清除執行緒Map裡所有key為null的value。所以最怕的情況就是,threadLocal物件設null了,開始發生“記憶體洩露”,然後使用執行緒池,這個執行緒結束,執行緒放回執行緒池中不銷燬,這個執行緒一直不被使用,或者分配使用了又不再呼叫get,set方法,那麼這個期間就會發生真正的記憶體洩露。

getEntry函式:

首先從ThreadLocal的直接索引位置獲取Entry e,如果e不為null並且key相同則返回e;如果e為null或者key不一致則通過getEntryAfterMiss向下一個位置查詢

     private Entry getEntry(ThreadLocal key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

getEntryAfterMiss函式:

這個過程中遇到的key為null的Entry都會被擦除(Entry內的value也就沒有強引用鏈,自然會被回收)

    private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal k = e.get();
                if (k == key)
                //命中
                    return e;
                if (k == null)
                //如果key值為null,則擦除該位置的Entry
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                //繼續向下一個位置查詢
                e = tab[i];
            }
            return null;
        }

set函式:
set操作也有類似的思想,將key為null的這些Entry都刪除,防止記憶體洩露

     private void set(ThreadLocal key, Object value) {

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

小結:

  • 雖然原始碼中對記憶體洩漏做了很好的防護作用,但是很多情況下還是需要手動呼叫ThreadLocal的remove函式,手動刪除不再需要的ThreadLocal,防止記憶體洩露。
  • 所以JDK建議將ThreadLocal變數定義成private static的,這樣的話ThreadLocal的生命週期就更長,由於一直存在ThreadLocal的強引用,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱引用訪問到Entry的value值,然後remove它,防止記憶體洩露。

InheritableThreadLocal與ThreadLocal的區別

InheritableThreadLocal比ThreadLocal多一個特性,繼承性,可以從父執行緒中得到初始值

首先瀏覽下 InheritableThreadLocal 類中有什麼東西:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    protected T childValue(T parentValue) {
        return parentValue;
    }
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

其實就是重寫了3個方法

  • InheritableThreadLocal的get()方法會呼叫getMap(t),而這時返回的是inheritableThreadLocals(Thread的一個成員變數)
  • 父執行緒往子執行緒中傳遞值是在Thread thread = new Thread()的時候,然後呼叫執行緒內部的init方法進行處理,最終就是不斷的把當前執行緒的inheritableThreadLocals值複製到我們新建立的執行緒中的inheritableThreadLocals 中
  • 主要面對的是執行緒中再建立執行緒的場景,類似開篇舉的例子,而對於子執行緒之間的傳遞或者執行緒池中得到父執行緒的值則不可行(這部分沒有深入研究)

總結

現在回過頭來分析開篇的例子:

  • 第一次列印:子執行緒中的ThreadLocal沒有賦值,所以為null,而子執行緒中的InheritableThreadLocal卻可以獲取到父執行緒中的值CONSTANT_01
  • 第二次列印:子執行緒ThreadLocal和InheritableThreadLocal同時重新賦值CONSTANT_02,所以打印出的結果都為CONSTANT_02
  • 第三次列印:回到主執行緒,主執行緒和子執行緒都是維護自己的副本,所以子執行緒賦值CONSTANT_02並不會對主執行緒有任何影響,所以主執行緒打印出的結果依舊都是CONSTANT_01

參考

其它


我的微信公眾號