第一次知道ThreadLocal是在看Looper源码的时候知道的,那时候只知道它的作用是让数据在各个线程单独保持一份,互不干扰,也一直没有去研究它的具体实现。昨天下班前粗略地看了一遍,我心里想的是“这玩意儿真的是太麻烦了,要是我的话,直接在线程里维护一个Object数组就能实现这个功能啊”。然后下了班回到家,我又仔仔细细的看了一遍,果然大佬还是你大佬,我还是太天真了。
在正式读代码前先简单介绍ThreadLocal的实现方式。每个线程都会有一个ThreadLocalMap,只有在使用到ThreadLocal的时候才会初始化ThreadLocalMap。需要存储的对象T会被放到Entry里面存储在ThreadLocalMap的数组中,Entry是一个键值对的数据结构,ThreadLocal实例为key,T为value。在使用的过程中,ThreadLocal会先找到当前线程的ThreadLocalMap,根据ThreadLocal的散列值找到存储的位置执行get方法或者set方法。
下面我画了一张图来说明。ThreadLocal本身不是用来存放数据的,真正用来存储的是线程内部的ThreadLocalMap,而ThreadLocal只是作为ThreadLocalMap中的key。
老样子,还是由一段简单的代码开始深入源码
final ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("你好");
Log.d("mark", "mark_1:" + threadLocal.get());
new Thread(new Runnable() {
@Override
public void run() {
Log.d("mark", "mark_2:" + threadLocal.get());
threadLocal.set("很高兴见到你");
Log.d("mark", "mark_3:" + threadLocal.get());
}
}).start();
05-31 16:58:30.878 19235-19235/com.newhongbin.lalala D/mark: mark_1:你好
05-31 16:58:30.879 19235-19626/com.newhongbin.lalala D/mark: mark_2:null
05-31 16:58:30.879 19235-19626/com.newhongbin.lalala D/mark: mark_3:很高兴见到你
首先在主线程中创建ThreadLocal对象,并set“你好”,在主线程中get,可以看到取到的就是刚才set的字符串;然后开启一个子线程,这时候子线程中还没有set过,所以取出来的是null,在子线程set过之后,就能够成功取出相应的字符串了。虽然是同一个ThreadLocal对象,但是在不同的线程中get到的数据是不一样的。
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);
}
逻辑很简单,取到当前线程的ThreadLocalMap,如果没有初始化过,就调用createMap初始化。初始化过程就是调用ThreadLocalMap的其中一个构造方法,我们来看看这个构造方法。
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
构造方法中会定义一个Entry数组,数组初始化容量为16,扩容因子为2/3,每次扩容为原来的2倍。Entry是WeakReference的子类,因此不会影响ThreadLocal对象的生命周期以及内存回收。Entry实现了键值对存储的功能,当前ThreadLocal对象为key,需要存储的对象为value。
static class Entry extends WeakReference<ThreadLocal> {
/** 与当前ThreadLocal相关联的值 */
Object value;
//ThreadLocal为key,真正需要存储的对象为value
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
初始化完Entry数组之后,需要计算当前ThreadLocal的散列值(hashcode),因为这里是第一个放入的Entry,不可能会发生hash碰撞,所以计算完hashcode之后,就直接把Entry放入数组下标为hashcode的位置上。最后计算出下一次需要扩容的临界值,即 (数组长度*2/3) 。到此为止,第一个值成功set。
那么问题又来了:
1、如果一个ThreadLocal在同一个线程中多次set呢?
2、如果多个ThreadLocal在同一个线程中的hashcode一样怎么办呢?
OK,回到刚开始的set方法,如果ThreadLocalMap不为null的情况。
private void set(ThreadLocal key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//发生hash碰撞时如果碰撞的位置上已经有Entry,且原有的key没有被回收,就查找数组下一个位置,如果没有Entry就放入
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
if (k == key) {//同一个ThreadLocal多次set,会直接覆盖原来的值
e.value = value;
return;
}
if (k == null) {//原来的ThreadLocal已经被回收了,就放入新的Entry
replaceStaleEntry(key, value, i);
return;
}
}
//在空的位置上放入Entry之前先判断是否需要扩容
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
set方法中关键的几个步骤我都在源码中加了注释,应该比较容易理解,这样就能回答上面的两个问题:
1、如果一个ThreadLocal在同一个线程中多次set呢?
直接覆盖原有的值。
如果发生碰撞的那个位置上的Entry的ThreadLocal被回收了,就放在碰撞的位置上;如果没有被回收,就寻找Entry下一个位置进行判断,直到找到ThreadLocal被回收的Entry,或者空的位置,放入。
get
前面的set其实已经把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();
}
如果当前线程在get之前已经初始化过ThreadLocalMap,那么就根据hashcode找到指定的Entry,返回value。
如果当前线程在get之前还未初始化过ThreadLocalMap,那么就返回setInitialValue()。
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
setInitialValue方法很简单,定义一个value指向null,如果ThreadLocalMap不为空,就插入value;如果ThreadLocalMap为空,先调用createMap初始化ThreadLoaclMap,再插入value。最后返回的就是value。
内存泄露
前面分析完get和set差不多就理解ThreadLocal的实现原理了,当然实际的代码还是比这更复杂一些的,ThreadLocalMap针对Entry数组还有清理方法,擦除方法,替换方法等,不过核心差不多,都是遍历查看Entry的key有效性,做出相应的处理,我就不再把代码展开了。但是可以看到这些方法里面都会清除ThreadLocal已经无效的那个value,这里面涉及到一些内存的问题,这里也来分析一下。
前面说了ThreadLocal作为Key是以弱引用的方式存储在Entry里面的,一旦发生GC,key就被回收了,那么value就无法被访问了,但是呢,value还有一条引用链,即“Thread->ThreadLocalMap->Entry->value”,所以value无法被回收,却也无法被访问,导致内存的泄露。为了尽量减少这种情况,在get、set等方法里面,都会去处理这一类key被回收的Entry。
resize
借着ThreadLocalMap也想聊聊扩容这个方法,一般的像HashMap、ThreadLocalMap等以键值对存储的容器类都有一个扩容方法,而且相似的是,容器的初始大小都是2^n,扩容也是2倍,这样设置的作用是啥呢?
所有的对象都有hashcode,而且一般来说不同的对象hashcode也不同,值的分布会相对比较均匀。那么来看看ThreadLocalMap计算存储下标的算法:
int i = key.threadLocalHashCode & (table.length - 1);
hashcode & (2^n-1)
假设n为4,即i = hashcode & 1111,只要hashcode在数组容量以内不相同,计算的数组下标i就不会相同。
换一个情况,如果容量不为2^n,假设为17,i = hashcode & 10000,这种情况下当hashcode大于0小于16,得到的数组下标i都是0,这样的分布是不是想想就觉得可怕。
ThreadLocal的hashcode也很有意思,第一次类加载的时候会初始化一个静态的AtomicInteger,后续每创建一个ThreadLocal都会改变这个AtomicInteger,这样就能够减少ThreadLocal的hashcode碰撞的概率。