一、简介
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
二、ThreadMap结构
每个Thread对象中保存一个ThreadLocalMap,ThreadLocal.set()时,取出当前线程的ThreadLocalMap(没有就创建),然后以ThreadLocal作为key,value作为值塞进去。这样一来Thread被销毁时,对应的ThreadLocalMap也会自动被回收。
三、ThreadLocalMap结构与原理
3.1 和HashMap的不同点
- ThreadLocalMap不像HashMap那样用数组和链表,他只有数组,当发生hash冲突时,就去找下一个空位置。扩容阈值为2/3。
- Entry key采用弱引用,在get、set、remove方法中会有清理过期entry的操作
- 因为是在单个线程内部操作,所以不存在线程安全问题。
3.2 弱引用key
Entry:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看到key是弱引用,主要是为了ThreadLocal不被强引用时,方便回收key。但是value是强引用的,所以还是可能存在key被回收了,value还在的情况,就是内存泄漏。在调用set(),get(),remove()方法的时候会清除过期的value值。
3.3 清理方法
在get()、set和remove方法中均有可能调用到清理方法,主要有expungeStaleEntry方法和cleanSomeSlots方法。
expungeStaleEntry方法(探测式清理):
沿着开始位置清理过期数据,沿途碰到未过期数据,将它rehash之后沿着原本应该的位置向后找到第一个空的slot放进去。
cleanSomeSlots方法(启发式清理):
cleanSomeSlots(int i, int n); 参数i是当前位置,n是table长度。
从当前位置的下一个(因为当前entry是null)开始遍历,循环结束条件 (n >>>=1) != 0, (如n == 16的话,循环5次)。如果遍历过程中遇到entry != null && entry.get() == null,即过期entry的话,那就执行expungeStaleEntry(i)。时间复杂度为对数级别,是介于不扫描和全扫描的一个折中,会保留一些垃圾。
3.4 get方法
get方法先寻找当前slot位置key是否等于当前ThreadLocal,是的话直接返回。否则向后查找,查找过程中遇到过期的entry,那就执行expungeStaleEntry方法清理。直到找到目标ThreadLocal,如果遇到Entry为空,那就返回null。
3.5 set方法
类似get方法,根据hash值从当前位置开始向后寻找,如果遇到key值等于当前ThreadLocal,则替换; 如果key值过期,则执行replaceStaleEntry填入;如果entry为null,则填入,然后执行cleanSomeSlots,如果没有清理掉一个垃圾,并且size达到扩容阈值了,那就执行rehash()扩容。
replaceStaleEntry方法逻辑:
- 先从当前位置A(过期的entry)向前遍历,直到遇到空entry,如果遇到过期entry,则记录下标,标记为清理开始位置。
- 从位置A向后遍历,直到遇到空entry,如果遇到key与当前ThreadLocal相等,那么填入value,再把当前entry与位置A的entry换位,然后先执行expungeStaleEntry,再执行cleanSomeSlots清理过期entry;如果没有找到key,那么就是遇到空entry了,直接new Entry并填入到位置A,然后同样先执行expungeStaleEntry,再执行cleanSomeSlots方法清理。
3.6 remove方法
类似get,set方法,也是将传入的ThreadLocal进行hash之后得到的下标,从此开始往后遍历直到空entry,如果找到entry的key等于传入的ThreadLocal,就执行entry.clear()清除引用,然后执行expungeStaleEntry(i),方法返回。
3.7 扩容
rehash方法:
先执行expungeStaleEntries方法清理掉table里所有过期entry(就是遍历所有entry执行expungeStaleEntry),如果清理完之后size仍然达到threshold的3/4那么执行resize()。
resize扩容方法:
创建一个当前size*2大小的table,遍历old table,如果遇到过期entry,则设置value=null,否则将entry里的key进行hash,放到new table里,如果new table里的位置不为null,就依次往后找直到找到一个最近的空位,放进去。