Android(安卓)Handler消息机制中的ThreadLocal源码分析
系列文章:
- Android消息机制Handler源码分析
- Android Handler消息机制中的ThreadLocal源码分析
1. 前言
ThreadLocal
定义如下:
public class ThreadLocal
可以看出其是一个泛型类,同一个 ThreadLocal 所包含的对象(对ThreadLocal< String>而言即为 String 类型变量),在不同的线程中有不同的副本(实际是不同的实例,后文会详细阐述)
每个线程内有自己的实例副本,且该副本只能由当前线程使用。这是也是 ThreadLocal 命名的由来
1.1 使用场景
ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。在Android中的Handler中便使用了,主要用来存放和获取不同线程中的Looper。
摘自Java进阶(七)正确理解Thread Local的原理与适用场景
2. Looper
在Android消息机制Handler一文中已经分析过Android的Handler机制,其中的Looper起到了消息循环器的作用,Looper的获取便使用到了ThreadLocal:
// Looper源码中sThreadLocal变量static final ThreadLocal sThreadLocal = new ThreadLocal();
大家注意,子线程默认是没有Looper的,而UI线程(ActivityThread)帮我们开启了Looper,因为ActivityThread被创建时就会初始化Looper,这也是在主线程中默认可以使用Handler的原因。这一点在Android消息机制Handler中已经分析过了。
2.1 创建Looper
首先看一下构造方法,在构造方法中它会创建一个MessageQueue消息队列,然后将当前线程的对象保存起来。
private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread();}
Handler的工作需要Looper,没有Looper的线程就会报错,那么如何为线程创建Looper呢?通过Looper.prepare()
即可为当前线程创建一个Looper,同时会将Looper保存在ThreadLocal
中,Handler的构造函数便是从ThreadLocal
中获取Looper
对象,从代码如下:
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));}
Looper除了prepare()
方法外,还提供了prepareMainLooper()
方法,这个方法主要是给主线程即ActivityThread
创建Looper使用的,本质也是通过prepare()
方法来实现的,源码如下:
public static void prepareMainLooper() { prepare(false); synchronized (Looper.class) { if (sMainLooper != null) { throw new IllegalStateException("The main Looper has already been prepared."); } sMainLooper = myLooper(); }}
在程序启动的时候,系统已经帮我们自动调用了Looper.prepare()
方法。ActivityThread
中的main()
方法调用了Looper.prepareMainLooper()
方法,而这个方法又会再去调用Looper.prepare()
方法。因此我们应用程序的主线程中会始终存在一个Looper对象,从而不需要再手动去调用Looper.prepare()
方法了。
由于主线程Looper比较特殊,所以Looper提供了一个getMainLooper()
方法,通过它可以在任何地方获取主线程的Looper。
2.2 为什么Handler中使用Looper
Looper的作用域就是线程并且不同线程具有不同的Looper,这个时候通过ThreadLocal就可以轻松实现Looper在线程中的存取,如果不采用ThreadLocal,那么系统就必须提供一个全局的哈希表供Handler查找指定线程的Looper,这样一来就必须提供一个类似于LooperManager的类了,但是系统并没有这么做而是选择了ThreadLocal,这就是ThreadLocal的好处。
摘自Android的消息机制之ThreadLocal的工作原理
3. ThreadLocal源码分析
我们注意到在Looper.prepare()
方法中,我们会先判断sThreadLocal.get()
方法为不为空,不为空再去调用sThreadLocal.set()
方法,下面我们就来具体分析下ThreadLocal的get和set方法的源码。
3.1 ThreadLocal.get
get方法源码如下:
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue();}
读取实例时,线程首先通过getMap(t)
方法获取线程自身的 ThreadLocalMap,ThreadLocalMap是ThreadLocal的内部类,真正保存了数据,同时每个线程中都有一个ThreadLocalMap对象,定义如下:
// Thread源码ThreadLocal.ThreadLocalMap threadLocals = null;
getMap(t)
源码:
ThreadLocalMap getMap(Thread t) { return t.threadLocals;}
其中直接返回当前线程的threadLocals
变量,但是线程默认该变量为null,因此第一次获取时肯定为null,所以get()方法便会直接调用setInitialValue()
方法:
private T setInitialValue() { T value = initialValue(); // 此方法直接返回null Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); // 再次获取map还是null if (map != null) map.set(this, value); else createMap(t, value); // 创建map return value;}
所以从上面看出,最后会调用createMap
创建ThreadLocalMap对象:
void createMap(Thread t, T firstValue) { // 初始化当前线程中的ThreadLocalMap对象 t.threadLocals = new ThreadLocalMap(this, firstValue);}
我们再回到get方法中,如果我们map != null
发生,那么从源码中可以看出会从ThreadLocalMap中获取一个Entry对象,从中获取value并返回,这说明我们真正的数据都是保存在ThreadLocalMap.Entry
中,后面我们再来具体分析。
3.2 ThreadLocal.set
接下来看下set方法源码:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value);}
类似get方法,首先获取当前线程ThreadLocalMap对象,如果不为空,就调用map.set
方法,如果为空就创建ThreadLocalMap,上面已经分析过了。
4. ThreadLocalMap
成员变量
// 初始容量,必须是2的整数次幂private static final int INITIAL_CAPACITY = 16;// Entry数组,必要时rehash,长度必须是2的整数次幂private Entry[] table;// table数组真正数据大小private int size = 0;// 容量阈值,达到此值需rehashprivate int threshold;
4.1 ThreadLocalMap.Entry
上面我们提到,真正保存数据的是ThreadLocalMap.Entry
对象,下面我们先来看下其定义:
static class Entry extends WeakReference> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; }}
可以看出,其构造函数接受两个参数,一个是ThreadLocal对象,另一个是待保存的数据。ThreadLocalMap 的每个 Entry 都是一个对键 的弱引用,这是为了防止内存泄漏, 对值的强引用。而且ThreadLocalMap中还定了一个Entry数组,可见最终的数据都是存放在此数组中:
// ThreadLocalMap源码private Entry[] table;
4.2 ThreadLocalMap.set
private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; // 获取到key对应在数组中的下标位置 int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { // 获取到的entry不为null // 获取保存在entry中的弱引用key,即ThreadLocal对象 ThreadLocal<?> k = e.get(); // 如果键值相等就更新value if (k == key) { e.value = value; return; } // 如果键值为null就通过 replaceStaleEntry 方法将所有键为 null // 的 Entry 的值设置为 null,从而使得该值可被回收 if (k == null) { replaceStaleEntry(key, value, i); return; } } // 上面获取到的entry为null,说明该位置还没有被初始化 // 初始化该位置的Entry tab[i] = new Entry(key, value); // table数组的大小+1 int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();}
关于set方法的解释基本都写在代码注释中了,如上。threadLocalHashCode & (size - 1)
的算法,这相当于取模运算threadLocalHashCode % size
的一个更高效的实现。
这里注意下在获取到key对应在数组中的下标位置时,是通过threadLocalHashCode
变量与table长度位与实现的,在ThreadLocalMap中的Entry的数组长度规定了必须是2的整数次幂,而threadLocalHashCode
是定义在ThreadLocal中的final变量,每个ThreadLocal对象的此变量是唯一的,threadLocalHashCode 是专门为 ThreadLocalMap 优化过的,可以减少哈希碰撞。
同时如果出现hash碰撞了,便会获取下一个index,下标不断加 1,不是 HashMap 的链地址法,采用的是线性探测法:
private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0);}
4.3 ThreadLocalMap.replaceStaleEntry(替换陈旧Entry)
上述set过程中,如果出现了key为null的情况,表明此entry为staled entry(陈旧的Entry),就将其替换为当前的key和value:
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; int slotToExpunge = staleSlot; // 从当前slot向前遍历 for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // 从当前slot向后遍历 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // If we find key, then we need to swap it // with the stale entry to maintain hash table order. // The newly stale slot, or any other stale slot // encountered above it, can then be sent to expungeStaleEntry // to remove or rehash all of the other entries in run. // 如果我们找到了key,那么我们替换它和stale Entry以保持table的顺序 if (k == key) { // 替换它和stale Entry e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // 向后扫描没有找到stale entry if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // key没有找到,则重新初始化 tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // 清理 if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}
4.4 ThreadLocalMap.getEntry
private Entry getEntry(ThreadLocal<?> key) { // 获取到key对应在数组中的下标位置 int i = key.threadLocalHashCode & (table.length - 1); // 获取entry Entry e = table[i]; // entry不为null,且键值相等就返回entry if (e != null && e.get() == key) return e; // entry为null, else return getEntryAfterMiss(key, i, e);}
关于getEntry的解释基本都写在代码注释中了,如上。
4.5 ThreadLocalMap.cleanSomeSlots
private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed;}
5. 总结
ThreadLocal.jpg上图总结了ThreadLocal具体是怎么保存Looper的,首先在Looper类中定义了sThreadLocal
变量,然后调用ThreadLocal的get/set来获取/设置当前线程Looper,get/set之前都是要先调用getMap获取当前线程的threadLocals
变量,threadLocals
对应的是ThreadLocalMap,get/set方法又分别调用的是ThreadLocalMap中的getEntry/set方法,ThreadLocalMap中真正保存数据的是Entry对象,其以ThreadLocal为弱key,传入的值为value。因为每个线程都有私有的threadLocals
变量,便可做到数据的线程隔离。
更多相关文章
- SpringBoot 2.0 中 HikariCP 数据库连接池原理解析
- 获取手机的mac地址
- Android多线程机制和Handler的使用
- android 初始化时获得控件位置,宽高的属性
- Android显式intent和隐式intent
- RXJava
- Android(安卓)AysncTask 从源码角度简单理解它的使用限制原因
- 2019-04-01
- Android(安卓)中Service组件