今天端午,就看一下轻松点的东西吧,上次说消息机制,说到Looper时,就是把Looper存储在ThreadLocal中,然后在对应的线程获取到对象,今天就来看下ThreadLocal的源码解读吧。
还是上次讲的那个例子
final ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); threadLocal.set(1); new Thread(new Runnable() { @Override public void run() { Log.d(TAG, "run: "+threadLocal.get()); } }).start(); 复制代码
得到的结果是***MainActivity: run: null,可见在哪个线程放在数据,只有在对应的那个线程取出。
每一个线程Thread的源码内部有属性
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; 复制代码
再看ThreadLocal的源码,主要关心的就是存储问题,也就是set和get方法,先来看下set
public void set(T value) { //获取当前线程 Thread t = Thread.currentThread(); //得到ThreadLocalMap,这是个专门用于存储线程的ThreadLocal的数据 ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } 复制代码
看第二行 ThreadLocalMap map = getMap(t);
,这是个专门用于存储线程的ThreadLocal的数据,set的步骤是:
ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { //每一个线程中都一个对应的threadLocal,然后又通过ThreadLocal负责来维护对应的ThreadLocalMap //通过ThreadLocal来获取来设置线程的变量值 t.threadLocals = new ThreadLocalMap(this, firstValue); } 复制代码
先暂时不去看ThreadLocalMap的源码,只要知道它是用于存储就行,我们先看下get方法
public T get() { Thread t = Thread.currentThread(); //还是能过当前线程get到这个threadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; //从map里取到值就直接返回 return result; } } //没有取到值就返回默认初始值 return setInitialValue(); } ThreadLocalMap getMap(Thread t) { //当前线程的ThreadLocal return t.threadLocals; } private T setInitialValue() { //这个等下看,看字面意思就是初始化value T value = initialValue(); Thread t = Thread.currentThread(); //下面的就是和set方法就是一样的了 ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } 复制代码
再看一下initialValue方法
/** * Returns the current thread's "initial value" for this * thread-local variable. This method will be invoked the first * time a thread accesses the variable with the {@link #get} * method, unless the thread previously invoked the {@link #set} * method, in which case the {@code initialValue} method will not * be invoked for the thread. Normally, this method is invoked at * most once per thread, but it may be invoked again in case of * subsequent invocations of {@link #remove} followed by {@link #get}. * * <p>This implementation simply returns {@code null}; if the * programmer desires thread-local variables to have an initial * value other than {@code null}, {@code ThreadLocal} must be * subclassed, and this method overridden. Typically, an * anonymous inner class will be used. * * @return the initial value for this thread-local */ protected T initialValue() { return null; } 复制代码
就返回了一个null,为什么不直接用null呢,这也是复制这一大段注释的原因,此实现只返回{@code null};如果程序员希望线程局部变量的初始值不是{@code null},则必须对{@code ThreadLocal}进行子类化,并且此方法将被重写。通常,将使用匿名内部类。
再回到get方法,可以得出get的步骤为:
现在我们集中精力来看 ThreadLocalMap
的源码
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } private static final int INITIAL_CAPACITY = 16; private Entry[] table; private int size = 0; private int threshold; } 复制代码
上面是ThreadLocalMap的一些属性,结构看起来和HashMap结构差不多,可以看到ThreadLocalMap的Entry继承自WeakReference,并使用ThreadLocal为键值。
这里为什么不使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程绑定,只要线程没有销毁,那么节点在GC是一直是处于可达状态,是没办法回收的,而程序本身并没有方法判断是否可以清理节点。弱引用的性质就是GC到达时,那么这个对象就会被回收。当某个ThreadLocal已经没有强引用可达,则随着它被GC回收,在ThreadLocalMap里对应的Entry就会失效,这也为Map本身垃圾清理提供了便利。
/** * Set the resize threshold to maintain at worst a 2/3 load factor. * 设置resize阈值以维持最坏2/3的负载因子 */ private void setThreshold(int len) { threshold = len * 2 / 3; } /** * Increment i modulo len. * 下一个索引 */ private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } /** * Decrement i modulo len. * 上一个索引 */ private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); } 复制代码
熟悉HashMap的话,其实对负载因子应该很熟悉,ThreadLocal有两个方法用于得到上/下一个索引,用于解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
所以很明显,ThreadLocalMap这种线性探测方式来解决Hash冲突效率很低,建议: 每个线程只存一个变量,这样的话所有的线程存放到map中的key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。
再来看它的set方法
private void set(ThreadLocal<?> key, Object value) { 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(); //找到对应的entry if (k == key) { e.value = value; return; } //替换失效的entry if (k == null) { replaceStaleEntry(key, value, i); return; } } //如果没有找到对应的key,就在末尾放上new Entry tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) //再次hash rehash(); } private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // Back up to check for prior stale entry in current run. // We clean out whole runs at a time to avoid continual // incremental rehashing due to garbage collector freeing // up refs in bunches (i.e., whenever the collector runs). //向前探测 int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; //固定步长 i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // Find either the key or trailing null slot of run, whichever // occurs first //向后遍历 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,更新为新的value if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists //如果在整个扫描过程中,找到了之前的无效值,那么以它为清理起点,否则以当前的i为清理起点 if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // If we didn't find stale entry on backward scan, the // first stale entry seen while scanning for key is the // first still present in the run. if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot //如果key在table中不存在,在原地放一个new entry tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them //在探测过程中发现无效的位置,则做一次清理(连续段清理+启发式清理) if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); } 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; } private void rehash() { //清理陈旧数据 expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis // 清理完后,如果>=3/4阈值,就进行扩容 if (size >= threshold - threshold / 4) resize(); } 复制代码
可以看到,和HashMap最大的不同在于,ThreadLocalMap的结构非常简单,没有next引用,就是说ThreadLocalMap解决Hash冲突并不是链表的方式,而是线性探测——当key的hashcode值在table数组的位置,如果发现这个位置上已经有其他的key值元素占用了,那么利用固定的算法寻找下一定步长的下一个位置,依次判断,直到找到能够存放的位置
对于ThreadLocal的内存泄漏,由于ThreadLocalMap的key是弱引用,而value是强引用,这就导致当ThreadLocal在没有外部对象强引用时,GC会回收key,但value不会回收,如果ThreadLocal的线程一直运行着,那么这个Entry对象的value就可能一直不能回收,引发内存泄漏。
ThreadLocal的设计已经为我们考虑到了这个问题,提供remove方法,将Entry节点和Map的引用关系移除,这样Entry在GC分析时就变成了不可达,下次GC就能回收。
看一下remove的源码
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } 复制代码
可以看到就是调用了ThreadLocalMap的remove方法
/** * 从map中删除threadLocal */ private void remove(ThreadLocal<?> key) { 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)]) { if (e.get() == key) { //调用entry的clear方法 e.clear(); //进行清理 expungeStaleEntry(i); return; } } } 复制代码
所以我们在使用后,可以显示的调用remove方法,来避免内存泄漏,是一个很好的编程习惯。
参考
ThreadLocal终极源码剖析-一篇足矣!
ThreadLocal-面试必问深度解析
Java并发编程:深入剖析ThreadLocal
我的CSDN
下面是我的公众号,欢迎大家关注我