第一次知道ThreadLocal是在看Looper源码的时候知道的,那时候只知道它的作用是让数据在各个线程单独保持一份,互不干扰,也一直没有去研究它的具体实现。昨天下班前粗略地看了一遍,我心里想的是“这玩意儿真的是太麻烦了,要是我的话,直接在线程里维护一个Object数组就能实现这个功能啊”。然后下了班回到家,我又仔仔细细的看了一遍,果然大佬还是你大佬,我还是太天真了。
在正式读代码前先简单介绍ThreadLocal的实现方式。每个线程都会有一个ThreadLocalMap,只有在使用到ThreadLocal的时候才会初始化ThreadLocalMap。需要存储的对象T会被放到Entry里面存储在ThreadLocalMap的数组中,Entry是一个键值对的数据结构,ThreadLocal实例为key,T为value。在使用的过程中,ThreadLocal会先找到当前线程的ThreadLocalMap,根据ThreadLocal的散列值找到存储的位置执行get方法或者set方法。
下面我画了一张图来说明。当定义ThreadLocal<A>对象,在不同线程中保存的A实例对象分别保存在各自线程的ThreadLocalMap中。
老样子,还是由一段简单的代码开始深入源码
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。
初始化完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方法很简单,定义一个null,如果ThreadLocalMap不为空,就插入null;如果ThreadLocalMap为空,先调用createMap初始化ThreadLoaclMap,再插入null。最后返回null。