转载

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

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

源码地址

手写ThreadLocal

起源

在Android的 handler消息机制looper 是怎么绑定线程的?为什么这样做可以达到绑定线程的目的?

想要解答并彻底理解这两个问题那就需要搞明白 ThreadLocal 到底是什么?它又是如何工作的?我们本篇的目的就是先搞明白这两个问题,然后回答上边的两个问题。

在开始之前我想先说这么一个观点:一般情况下,大家学习一个 源码原理 的时候,都是通过解读甚至精读源码来 学会一个原理 ,我认为这是 背诵式 的学习方式,就好比源码写的是一、二、三,我们通过读懂了『一、二、三』从而学会了、知道了他的实现原理,但我们容易忽略 本质 的问题,就是源码的实现要解决的问题是什么?为什么它这么做就能解决问题?如果让我们做是否能想到这样的方案或者其它的方案?所以对于一些原理性的知识,我认为如果我们能从 本质问题 出发,从 演化 的角度去思考它解决问题的方式、去模拟(手写代码)它解决问题的过程,甚至去思考扩展其它的解决方案,我想这样得来的知识才是 透彻的想忘都忘不掉的

ThreadLocal到底是什么?

ThreadLocal 的存在肯定是在解决某个问题的,所以这个问题是什么呢?

问题是:如何将数据与线程绑定起来,从而该数据只能在绑定的线程里访问,而其它线程无法访问?

ThreadLocal 能很方便的解决这个问题,这也就是所谓的 线程间数据隔离Local 这个单词有『局部的』意思,并且在源码首行注释中已写明『This class provides thread-local variables.(该类提供线程局部变量)』,所以 ThreadLocal 的最佳理解是 线程局部变量辅助器 ,通过它能很方便的 设置或者获取线程私有的数据 。而线程私有的数据也被美其名曰 线程局部变量

ThreadLocal是如何工作的?

思考分析

NOTE:该思路与源码是一致的,请放心食用,我们重在复现并理解思路的演化过程。

我们已经了解了问题,那换做我们会如何思考解决呢 ?现在有这么个思路:

  1. Thread 本身就是个线程对象,可以在其内部用一个 Map 数据结构来存储要绑定的数据
    MockThread
    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原理

可以看到测试结果都是符合我们预期的,至此本篇的主要工作就结束了,希望大家都能 在不用背的前提下 掌握了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数据结构中。

原文  https://juejin.im/post/5d6d32dbe51d4561ac7bcd1b
正文到此结束
Loading...