文章目录
- 四种引用的概念
- Entry的类定义
- 弱引用和内存泄漏
- 如果key使用强引用
- 如果key使用弱引用
- 为什么使用弱引用
- hash冲突的解决
- ThreadLocalMap中的set方法
- 演示垃圾回收
- get触发GC
- set触发GC
- ThreadLocal-内存清理
- 探测式清理(ExpungeStaleEntry)
- 启发式清理(cleanSomeSlots)
- ReplaceStaleEntry
- rehash(全局清理)
- 学习资料链接:
- 视频和文档资料1:
- 视频和文档资料2:
ThreadLocal的set,get方法只是作为一个接口,实质上还是对Thread里面的ThreadLocalMap里面的进行操作

jdk1.8之后,每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
Thread t = new Thread();
Map map = getMap(t);//根据线程获取他的threadLocalMap
map.set(new ThreadLocal<xxx>(),new Object());
四种引用的概念
Entry的类定义
/** Entry继承WeakReference,并且用ThreadLocal作为key.* 如果key为null(entry.get() == null),意味着key不再被引用,* 因此这时候entry也可以从table中清除。* entry.get()返回的是ThreadLocal类型的key*/
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}
弱引用和内存泄漏
强引用(“Strong” Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
弱引用(WeakReference),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
如果key使用强引用
假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄漏吗?
此时ThreadLocal的内存图(实线表示强引用)如下:
假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。
但是因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收。
在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。
也就是说,ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。
如果key使用弱引用
同样假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。
由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。
但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。
也就是说,ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。
其实不管是强引用还是弱引用,都会漏内存,只不过是多少的问题,只要忘记remove了,强引用是漏了kv,弱引用漏了v。只能说有点改善,但是不多
为什么使用弱引用
根据刚才的分析, 我们知道了: 无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。
要避免内存泄漏有两种方式:
-
使用完ThreadLocal,调用其remove方法删除对应的Entry
-
使用完ThreadLocal,当前Thread也随之运行结束
相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。
也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。那么为什么key要用弱引用呢?
事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。
这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。
最主要啊,是弱引用会导致key为null,如果key为null了,就说明这个Entry过期了,就要进行垃圾回收了
hash冲突的解决
public void set(T value) {Thread t = Thread.currentThread();ThreadLocal.ThreadLocalMap map = getMap(t);if (map != null)//调用了ThreadLocalMap的set方法map.set(this, value);elsecreateMap(t, value);}ThreadLocal.ThreadLocalMap getMap(Thread t) {return t.threadLocals;}void createMap(Thread t, T firstValue) {//调用了ThreadLocalMap的构造方法t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);}
这个方法我们刚才分析过, 其作用是设置当前线程绑定的局部变量 :
A. 首先获取当前线程,并根据当前线程获取一个Map
B. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
(这里调用了ThreadLocalMap的set方法)
C. 如果Map为空,则给该线程创建 Map,并设置初始值
(这里调用了ThreadLocalMap的构造方法)
这段代码有两个地方分别涉及到ThreadLocalMap的两个方法, 我们接着分析ThreadLocalMap这两个方法。
/** firstKey : 本ThreadLocal实例(this)* firstValue : 要保存的线程本地变量*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//初始化tabletable = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];//计算索引(重点代码)int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//设置值table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);size = 1;//设置阈值setThreshold(INITIAL_CAPACITY);}
构造函数首先创建一个长度为16的Entry数组,然后计算出firstKey对应的索引,然后存储到table中,并设置size和threshold。
a. 关于firstKey.threadLocalHashCode
private final int threadLocalHashCode = nextHashCode();private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}
//AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,适合高并发情况下的使用private static AtomicInteger nextHashCode = new AtomicInteger();//特殊的hash值private static final int HASH_INCREMENT = 0x61c88647;
这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647
,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中,这样做可以尽量避免hash冲突。每多一个ThreadLocal,都递增一次HASH_INCREMENT
b. 关于& (INITIAL_CAPACITY - 1)
计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,使得hash发生冲突的次数减小。
ThreadLocalMapset_168">ThreadLocalMap中的set方法
private void set(ThreadLocal<?> key, Object value) {ThreadLocal.ThreadLocalMap.Entry[] tab = table;int len = tab.length;//计算索引(重点代码,刚才分析过了)int i = key.threadLocalHashCode & (len-1);/*** 使用线性探测法查找元素(重点代码)*/for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();//ThreadLocal 对应的 key 存在,直接覆盖之前的值if (k == key) {e.value = value;return;}// key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,// 当前数组中的 Entry 是一个陈旧(stale)的元素if (k == null) {//用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏replaceStaleEntry(key, value, i);return;}}//ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry。tab[i] = new Entry(key, value);int sz = ++size;/*** cleanSomeSlots用于清除那些e.get()==null的元素,* 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。* 如果没有清除i往后任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行 * rehash(执行一次全表的扫描清理工作)*/if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}/*** 获取环形数组的下一个索引*/private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}
代码执行流程:
-
A. 首先还是根据key计算出索引 i,然后查找i位置上的Entry,
-
B. 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值,
-
C. 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry,
-
D. 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。
最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz 是否>= thresgold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理。
重点分析 : ThreadLocalMap使用线性探测法
来解决哈希冲突的。
该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。
举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。
按照上面的描述,可以把Entry[] table看成一个环形数组。
演示垃圾回收
get触发GC
get(c)触发GC,恰好是要获取1位置的value,↓↓↓↓
get(c)之后,原来key为null,value为1,现在key为c,value为null
set触发GC
清理完的效果
针对以上探讨的情况,我们推荐使用remove来清理内存,set,get的方式比较局限
ThreadLocal_258">ThreadLocal-内存清理
英文解释:
Expunge:清理,清除
Stale:过期的,失效的
探测式清理(ExpungeStaleEntry)
ExpungeStaleEntry返回下一个Entry为null的位置i
启发式清理(cleanSomeSlots)
Slot:槽,就是哈希槽的意思
cleanSomeSlot的调用背景是基于set
ReplaceStaleEntry
替换旧的无效节点
目的在于基于staleSlot位置,往前找,直到遇到第一个Entry为null的位置,往前找的过程中记录最前面过期Entry的位置slotToExpunge
往前找完了之后往StaleSlot后面找,直到遇到第一个Entry为null的位置,往后找的过程中清理所有过期Entry
replaceStaleEntry的背景是基于set的调用
rehash(全局清理)
学习资料链接:
视频和文档资料1:
通过网盘分享的文件:视频-由浅入深,全面解析ThreadLocal
链接: https://pan.baidu.com/s/1_CtLTXjBlQ1LlqySTmWpwg?pwd=atfs 提取码: atfs
–来自百度网盘超级会员v8的分享
通过网盘分享的文件:资料-ThreadLocal.zip
链接: https://pan.baidu.com/s/15Vz0WJymEtahX8FL1WNqnQ?pwd=pedv 提取码: pedv
–来自百度网盘超级会员v8的分享
【黑马程序员Java基础教程由浅入深全面解析threadlocal】 https://www.bilibili.com/video/BV1N741127FH/?p=12&share_source=copy_web&vd_source=afbacdc02063c57e7a2ef256a4db9d2a
【黑马Java面试八股文教程,大厂面试必会100题之Java并发之ThreadLocal】 https://www.bilibili.com/video/BV1gs4y137dL/?p=2&share_source=copy_web&vd_source=afbacdc02063c57e7a2ef256a4db9d2a
视频和文档资料2:
【透彻理解ThreadLocal,最新最全的面试题及解答】 https://www.bilibili.com/video/BV1L89wYzEc2/?share_source=copy_web&vd_source=afbacdc02063c57e7a2ef256a4db9d2a
通过网盘分享的文件:带你透彻理解Threadlocal.pdf
链接: https://pan.baidu.com/s/1jNfmpwChXb_qtZqk_F7ybg?pwd=xwyp 提取码: xwyp
–来自百度网盘超级会员v8的分享