写在前面
本次使用的源码来自于Android API 25 Platform.
引子
在Android中,在子线程中创建Handle人的时候,要求我们先调用Looper#prepare方法,来创建Looper,并且,一个线程中只有一个Looper。这是怎么实现的呢?
static final ThreadLocal sThreadLocal = new ThreadLocal();
public static void prepare() {
prepare(true);
}
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中使用ThreadLocal来保存每个线程中新创建的Looper,并且每次创建前先检查ThreadLocal中是否存在了Looper,以此来确保线程中Looper的唯一性。
那么,这里就有一个问题了,ThreadLocal能保证线程中的Looper是唯一,它是怎么做到这一点的呢?
ThreadLocal源码分析
在上面的Looper中,我们看到有使用ThreadLocal#set和ThreadLocal#get,所以,我们先从这两个方法开始吧。
ThreadLocal#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);
}
在这个方法中我们发现,数据实际上保存在ThreadLocalMap中, 如果先通过ThreadLocal#getMap来获取ThreadLocalMap,如果有ThreadLocalMap,则直接存储,没有则调用ThreadLocal#createMap。
在这里,我们先不去看什么是ThreadLocalMap, 只要记住ThreadLocal保存的数据被传进了ThreadLocalMap就行了,具体ThreadLocalMap是什么情况我们一会再看。现在我们先来看看ThreadLocal#getMap和ThreadLocal#createMap
ThreadLocal#getMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
首先,参数Thread是在ThreadLocal#set中我们通过Thread#currentThread获取的当前线程,该方法很简单,直接返回了当前线程的成员变量threadLocals, 那我们就进Thread去看看Thread#threadLocals是什么情况
Thread#threadLocals
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
这就更简单了,就是成员变量,没什么东西,那我们继续往下看。
ThreadLocal#createMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocal#createMap 就是给当前线程的成员变量Thread#threadLocals赋值。
现在我们应该已经有点眉目了,刚开始,直观上,觉得数据是保存在ThreadLocal中的,到这里我们可以判断,数据实际上是保存在当前线程中的,通过Thread#threadLocals持有数据。
我们继续往下看,看看这种判断是否正确。
ThreadLocal#get
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
通过上面的取数据,我们大致印证了刚才的判断,就是数据实际上被存储到当前线程中,因为取数据正是从正式我们当前线程的成员变量Thread#threadLocals中取。
这里,我们已经可以解释刚开始的那个问题了,为什么ThreadLocal能保证线程中Looper的唯一性,那是因为通过ThreadLocal,数据实际上被保存在每个线程中,每个线程的Thread#threadLocals是独立互不干扰的,所以他们保存在Thread#threadLocals中的数据也算是独立互不干扰的,也就可以保证Looper的唯一性。
下面,我们继续研究ThreadLocal.ThreadLocalMap。研究ThreadLocalMap我们 也从上面曾经出现过的ThreadLocalMap的三个方法1.ThreadLocalMap#set 2.ThreadLocalMap#getEntry 3.构造方法 开始研究。
在此之前,我们先了解一下ThreadLocal.ThreadLocalMap.Entry
ThreadLocal.ThreadLocalMap.Entry
static class Entry extends WeakReference {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
代码很简单,就是继承自WeakReference的数据类,类中有两个参数,一个是ThreadLocal,另外一个正是我们要保存的数据,类型是Object。
ThreadLocal.ThreadLocalMap#set
private Entry[] table;
private void set(ThreadLocal key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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();
//如果数组中i位置的元素的变量ThreadLocal与要设置的数据的ThreadLocal一样,
//则直接将i位置元素的值value替换为设置的数据
if (k == key) {
e.value = value;
return;
}
//如果数组中i位置的元素的变量ThreadLocal已经是null(为什么会出现null,
//因为Entry 继承自WeakReference,以弱引用的方式持有ThreadLocal),
//那么,直接替换位置i的元素,设置进来的数据将被保存在这个位置
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//最后,这是正常的情况了,数据被保存在数组中首次没有元素值得位置,比如数组长度20,
//只有前10个位置存有数据,那么,新来的数据就被放到了数组中角标为10的位置。
tab[i] = new Entry(key, value);
int sz = ++size;
//检查数组长度是否超过阀值,超过的话,对数组进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocalMap通过一个数组来保存数据。
ThreadLocalMap#getEntry
private Entry getEntry(ThreadLocal key) {
//获取数据在数组中的位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//获取i位置的元素,如果元素中的ThreadLocal不为空且和要获取数据的ThreadLocal一样
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
ThreadLocalMap#getEntryAfterMiss
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)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
这里,遍历数组,如果数组中存在某个元素的ThreadLocal跟key一样且不是null,就返回这个元素,如果元素中的ThreadLocal为null,则调用ThreadLocalMap#expungeStaleEntry,将这个位置的元素置为null。
感觉跑偏了啊,不要在意这些细节,我们再总结一下,ThreadLocalMap里面通过一个数据来存放数据,就这么简单。
下面我们继续看ThreadLocalMap的构造函数
ThreadLocal.ThreadLocalMap#ThreadLocalMap(java.lang.ThreadLocal, java.lang.Object)
private static final int INITIAL_CAPACITY = 16;
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
//初始化保存数据的数组,且数组初始长度16
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//保存第一个元素到数组中
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
构造函数中做的事情很简单,就是初始化了长度为16的数组,然后保存第一个元素。
另外,这个数组并不是定长的,我们可以在ThreadLocal.ThreadLocalMap#rehash中看到它的扩容逻辑
ThreadLocal.ThreadLocalMap#rehash
private void rehash() {
//先遍历数组,移除掉数组中为null的元素和不为null但是元素的变量ThreadLocal为null的元素
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
//threshold 为数组长度的2/3,这里,如果新添加元素后,长度不小于数组长度的一半,就进行扩容
}
/**
* Double the capacity of the table.
*/
private void resize() {
//扩容的逻辑很简单,直接将原数组的长度增加一倍
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
...
table = newTab;
}
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
ThreadLocal.ThreadLocalMap#rehash正是在ThreadLocal.ThreadLocalMap#set的最后调用的。每次添加新元素后,检查添加后数组长度是否大于阀值,如果大于阀值,直接将原数组长度增加一倍。
总结
- ThreadLocal并不保存数据,数据实际保存在当前线程中(java.lang.Thread#threadLocals)
- Thread#threadLocals中实际是通过数组来保存数据