手写ThreadLocal
在Android的 handler消息机制
中 looper
是怎么绑定线程的?为什么这样做可以达到绑定线程的目的?
想要解答并彻底理解这两个问题那就需要搞明白 ThreadLocal
到底是什么?它又是如何工作的?我们本篇的目的就是先搞明白这两个问题,然后回答上边的两个问题。
在开始之前我想先说这么一个观点:一般情况下,大家学习一个 源码原理
的时候,都是通过解读甚至精读源码来 学会一个原理 ,我认为这是 背诵式 的学习方式,就好比源码写的是一、二、三,我们通过读懂了『一、二、三』从而学会了、知道了他的实现原理,但我们容易忽略 本质 的问题,就是源码的实现要解决的问题是什么?为什么它这么做就能解决问题?如果让我们做是否能想到这样的方案或者其它的方案?所以对于一些原理性的知识,我认为如果我们能从 本质问题 出发,从 演化 的角度去思考它解决问题的方式、去模拟(手写代码)它解决问题的过程,甚至去思考扩展其它的解决方案,我想这样得来的知识才是 透彻的 、 想忘都忘不掉的 。
ThreadLocal
的存在肯定是在解决某个问题的,所以这个问题是什么呢?
ThreadLocal
能很方便的解决这个问题,这也就是所谓的 线程间数据隔离 。 Local
这个单词有『局部的』意思,并且在源码首行注释中已写明『This class provides thread-local variables.(该类提供线程局部变量)』,所以 ThreadLocal
的最佳理解是 线程局部变量辅助器
,通过它能很方便的 设置或者获取线程私有的数据 。而线程私有的数据也被美其名曰 线程局部变量
。
NOTE:该思路与源码是一致的,请放心食用,我们重在复现并理解思路的演化过程。
我们已经了解了问题,那换做我们会如何思考解决呢 ?现在有这么个思路:
Thread
本身就是个线程对象,可以在其内部用一个 Map
数据结构来存储要绑定的数据 MockThread Map
MockThreadLocal
类作为辅助器,专门用来操作 当前线程 里的 Map
set
get
remove
三个API方法 MockThreadLocal
的实例作为 Map
的 key
,并且 key
需要使用 弱引用 进行一次包装
MockThreadLocal
的实例就可以很方便的 set
get
remove
数据 MockThreadLocal
的生命周期将和 MockThread
一样长,需要做防止内存泄漏的处理 MockThreadLocal
类上定义泛型,该泛型用于存储到 Map
里的 Value
的类型 思路已确定,接下来手写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如下:
mtl1
直接调用 get
方法的结果(预期输出:null) mtl1.set("二娃_")
后,测试 mtl1
调用 get
方法的结果(预期输出:二娃_) mtl1.remove()
后,测试 mtl1
调用 get
方法的结果(预期输出:null) mtl2
直接调用 get
方法的结果(预期输出:false) mtl2.set(true)
后,测试 mtl2
调用 get
方法的结果(预期输出:true) 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原理。撒花!撒花!
经过前面的一通操作解答文头的两个问题就是手到擒来的事了
在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)); } 复制代码
为什么这样做可以达到绑定线程的目的?
这就是 ThreadLocal
的原理部分, ThreadLocal本就是设置或者获取线程私有数据的辅助类 ,通过它可以很方便的把数据存储到 当前线程 内部持有的Map数据结构中。