【不用背的原理】不用背的ThreadLocal原理

源碼地址

手寫ThreadLocaljava

起源

在Android的handler消息機制looper是怎麼綁定線程的?爲何這樣作能夠達到綁定線程的目的?git

想要解答並完全理解這兩個問題那就須要搞明白ThreadLocal究竟是什麼?它又是如何工做的?咱們本篇的目的就是先搞明白這兩個問題,而後回答上邊的兩個問題。github

在開始以前我想先說這麼一個觀點:通常狀況下,你們學習一個源碼原理的時候,都是經過解讀甚至精讀源碼來學會一個原理,我認爲這是背誦式的學習方式,就比如源碼寫的是1、2、三,咱們經過讀懂了『1、2、三』從而學會了、知道了他的實現原理,但咱們容易忽略本質的問題,就是源碼的實現要解決的問題是什麼?爲何它這麼作就能解決問題?若是讓咱們作是否能想到這樣的方案或者其它的方案?因此對於一些原理性的知識,我認爲若是咱們能從本質問題出發,從演化的角度去思考它解決問題的方式、去模擬(手寫代碼)它解決問題的過程,甚至去思考擴展其它的解決方案,我想這樣得來的知識才是透徹的想忘都忘不掉的數據結構

ThreadLocal究竟是什麼?

ThreadLocal的存在確定是在解決某個問題的,因此這個問題是什麼呢?app

問題是:如何將數據與線程綁定起來,從而該數據只能在綁定的線程裏訪問,而其它線程沒法訪問?ide

ThreadLocal能很方便的解決這個問題,這也就是所謂的線程間數據隔離Local這個單詞有『局部的』意思,而且在源碼首行註釋中已寫明『This class provides thread-local variables.(該類提供線程局部變量)』,因此ThreadLocal的最佳理解是線程局部變量輔助器,經過它能很方便的設置或者獲取線程私有的數據。而線程私有的數據也被美其名曰線程局部變量oop

ThreadLocal是如何工做的?

思考分析

NOTE:該思路與源碼是一致的,請放心食用,咱們重在復現並理解思路的演化過程。佈局

咱們已經瞭解了問題,那換作咱們會如何思考解決呢🤔?如今有這麼個思路:post

  1. Thread自己就是個線程對象,能夠在其內部用一個Map數據結構來存儲要綁定的數據
    1. 就是這種簡單的方式,讓數據與線程關聯起來,就能夠達到與線程綁定的目的
    2. 爲了模擬咱們定義一個MockThread
    3. 在內部定義一個Map數據結構類型的成員變量
  2. 咱們定義一個MockThreadLocal類做爲輔助器,專門用來操做當前線程裏的Map
    1. 由於每次操做的都是當前線程,因此就達到了隔離的目的
    2. 咱們對外暴露set get remove三個API方法
  3. 咱們將使用MockThreadLocal的實例做爲Mapkey,而且key須要使用弱引用進行一次包裝
    1. 這樣一來對外部使用者來講,只要有MockThreadLocal的實例就能夠很方便的set get remove 數據
    2. 由於MockThreadLocal的生命週期將和MockThread同樣長,須要作防止內存泄漏的處理
  4. 咱們將在MockThreadLocal類上定義泛型,該泛型用於存儲到Map裏的Value的類型

思路已肯定,接下來手寫ThreadLocal!學習

手寫ThreadLocal

先寫下MockThread類,這個比較簡單。須要注意的是ThreadLocalMap是定義在MockThreadLocal類中的。

class MockThread(target: Runnable, name: String) : Thread(target, name) {
    //用於保存綁定到線程上的數據
    var threadLocals: MockThreadLocal.ThreadLocalMap? = null
}
複製代碼

再寫下MockThreadLocal類,代碼自己並無難度,咱們以get方法爲例分析一把(我把詳細的註釋加到了代碼上)。

open class MockThreadLocal<T> {

    /** * 往當前線程上綁定數據 */
    fun set(value: T) {
        val t = Thread.currentThread() as MockThread
        val map = getMap(t)
        if (map != null)
            map.set(this, value as Any?)
        else
            createMap(t, value)
    }

    /** * 獲取在當前線程上綁定的數據 */
    fun get(): T? {
        // 獲取當前的線程
        val t = Thread.currentThread() as MockThread
        // 獲取當前線程持有的ThreadLocalMap
        val map = getMap(t)
        if (map != null) {
            // 若是map不爲null,就使用本身做爲key來獲取value(MockThreadLocal的實例)
            val e = map.get(this)
            if (e != null) {
                return e as T?
            }
        }
        // 若是map爲null,設置初始化的值,並返回該值
        return setInitialValue()
    }

    /** * 移除在當前線程上綁定的數據 */
    fun remove() {
        val m = getMap(Thread.currentThread() as MockThread)
        m?.remove(this)
    }

    /** * 設置初始化的值 */
    private fun setInitialValue(): T? {
        val value = initialValue()
        val t = Thread.currentThread() as MockThread
        val map = getMap(t)
        if (map != null)
            map.set(this, value as Any?)
        else
            createMap(t, value)
        return value
    }

    /** * 默認初始化的值,子類可複寫該方法,自定義初始化值 */
    open fun initialValue(): T? {
        return null
    }

    /** * 建立數據保存類,並賦值給線程 */
    private fun createMap(t: MockThread, value: T?) {
        t.threadLocals = ThreadLocalMap(this, value as Any?)
    }

    /** * 獲取線程中的數據保存類 */
    private fun getMap(t: MockThread): ThreadLocalMap? {
        return t.threadLocals
    }
    
    ... 省略ThreadLocalMap相關代碼
}
複製代碼

最後就是寫下ThreadLocalMap類,該類是實際保存、處理數據的類,代碼一樣沒有難度。其中一個重點就是對弱引用的處理,每次都要嘗試清除無用數據,來儘可能避免內存泄漏。

open class MockThreadLocal<T> {

    ... 省略代碼

    /** * 定義該類,用於實際保存數據、處理數據 */
    class ThreadLocalMap(firstKey: MockThreadLocal<*>, firstValue: Any?) {
        private var mMap: MutableMap<WeakReference<MockThreadLocal<*>>, Any?>? = null

        init {
            //首次初始化時,設置初始化值
            mMap = mutableMapOf(WeakReference(firstKey) to firstValue)
        }

        /** * 設置一個存儲的數據 */
        fun set(key: MockThreadLocal<*>, value: Any?) {
            //優先清除一次無用數據,防止內存泄漏
            expungeStaleEntry()
            if (mMap != null) {
                var keyExist = false
                mMap!!.forEach { (k, _) ->
                    //若相應的key已存在,只需替換該value便可
                    if (k.get() == key) {
                        mMap!![k] = value
                        keyExist = true
                    }
                }

                //若相應的key不存在,則保存新的數據
                if (!keyExist) {
                    mMap!![WeakReference(key)] = value
                }
            }
        }

        /** * 獲取一個存儲的數據 */
        fun get(key: MockThreadLocal<*>): Any? {
            //優先清除一次無用數據,防止內存泄漏
            expungeStaleEntry()
            mMap?.forEach { (k, v) ->
                if (k.get() == key) {
                    return v
                }
            }
            return null
        }

        /** * 移除一個存儲的數據 */
        fun remove(key: MockThreadLocal<*>) {
            //優先清除一次無用數據,防止內存泄漏
            expungeStaleEntry()
            mMap?.forEach { (k, _) ->
                if (k.get() == key) {
                    mMap?.remove(k)
                }
            }
        }

        /** * 清除key的實際值(MockThreadLocal)已被GC回收的數據,防止內存泄漏 * NOTE:當最後一次MockThreadLocal使用完後,一個好的習慣是主動調用remove方法移除綁定的數據, * 若不調用,那麼本方法將再無機會被調用,依舊有內存泄漏的可能。 */
        private fun expungeStaleEntry() {
            mMap?.forEach { (k, _) ->
                if (k.get() == null) {
                    mMap!!.remove(k)
                }
            }
        }
    }
}
複製代碼

到這裏咱們的代碼就寫完了,能夠發現ThreadLocal的工做原理,不但沒有難度,甚至簡單的使人感到意外。須要注意的是源碼中ThreadLocalMap沒有像我同樣直接使用的HashMap,但整體原理思路是一致的,這部分你們能夠食用源碼來了解

測試

對咱們的『小輪子』進行測試一把,看是否符合咱們的預期。咱們定義兩個MockThreadLocal變量mtl1 mtl2和兩個MockThread線程。

測試case以下:

  1. 在線程1中測試mtl1直接調用get方法的結果(預期輸出:null)
  2. 在線程1中先調用mtl1.set("二娃_")後,測試mtl1調用get方法的結果(預期輸出:二娃_)
  3. 在線程1中先調用mtl1.remove()後,測試mtl1調用get方法的結果(預期輸出:null)
  4. 在線程2中測試mtl2直接調用get方法的結果(預期輸出:false)
  5. 在線程2中先調用mtl2.set(true)後,測試mtl2調用get方法的結果(預期輸出:true)
  6. 在線程1內進行Thread.sleep(200)操做以保證在線程2先執行完的環境下,在線程2中測試mtl1直接調用get方法的結果(預期輸出:null)

測試代碼以下:

//定義兩個MockThreadLocal
val mtl1 = MockThreadLocal<String>()
val mtl2 = object : MockThreadLocal<Boolean>() {
    override fun initialValue(): Boolean? {
        return false
    }
}

//測試按鈕點擊時執行
btnRun.setOnClickListener {
    val thread1 = MockThread(Runnable {
        val name1 = Thread.currentThread().name

        //mtl1未設置值
        log2Logcat("$name1 mtl1未設置值時:mtl1.get()=${mtl1.get()}")

        //mtl1設置值:二娃_
        mtl1.set("二娃_")
        log2Logcat("$name1 mtl1設置值後:mtl1.get()=${mtl1.get()}")

        Thread.sleep(200)

        //mtl1調用remove
        mtl1.remove()
        log2Logcat("$name1 mtl1調用remove後:mtl1.get()=${mtl1.get()}")

        log2Logcat("$name1 線程運行結束---------------------")
    }, "線程1")

    val thread2 = MockThread(Runnable {
        val name2 = Thread.currentThread().name

        //mtl2未設置值
        log2Logcat("$name2 mtl2未設置值時:mtl2.get()=${mtl2.get()}")

        //mtl2設置值:true
        mtl2.set(true)
        log2Logcat("$name2 mtl2設置值後:mtl2.get()=${mtl2.get()}")

        log2Logcat("$name2 獲取mtl1的值:mtl1.get()=${mtl1.get()}")

        log2Logcat("$name2 線程運行結束---------------------")
    }, "線程2")

    thread1.start()
    thread2.start()
}
複製代碼

測試結果以下:

能夠看到測試結果都是符合咱們預期的,至此本篇的主要工做就結束了,但願你們都能在不用背的前提下掌握了ThreadLocal原理。撒花!撒花!

問題解答

通過前面的一通操做解答文頭的兩個問題就是手到擒來的事了

  1. 在Android的handler消息機制looper是怎麼綁定線程的?

    這裏確定是使用threadLocal的set方法綁定的,系統源碼以下

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }
    複製代碼
  2. 爲何這樣作能夠達到綁定線程的目的?

    這就是ThreadLocal的原理部分,ThreadLocal本就是設置或者獲取線程私有數據的輔助類,經過它能夠很方便的把數據存儲到當前線程內部持有的Map數據結構中。

文末

我的能力有限,若有不正之處歡迎你們批評指出,我會虛心接受並第一時間修改,以不誤導你們

個人其它文章

相關文章
相關標籤/搜索