一、ThreadLocal概述
二、ThreadLocal的使用方式
-
ThreadLocal的使用方法很简单,如下面所示: ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set("xz");
threadLocal.get();
-
由上面代码示例可知,要看源码出发点自然就是set和get方法了。
三、ThreadLocal源码分析
3.1、ThreadLocal、Thread、ThreadLocalMap、Entry之间的关系
-
四者之间的关系如下图所示:
3.2、ThreadLocal的set(T value)方法
-
源码和注释如下所示: -
上面截图中红框中的代码,会是我们下面着重要介绍的。 -
当我们创建ThreadLocal后,第一次调用set方法赋值的时候,由于ThreadLocalMap还没有被创建,所以会执行createMap(t, value)方法来对ThreadLocalMap进行初始化。其中,源码和注释如下所示: void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
-
从上面源码中我们可以看到,ThreadLocalMap是当前线程Thread的一个全局变量。从这里,我们就可以看出来,为什么说ThreadLocal是当前线程的本地变量了。 -
而在ThreadLocalMap的构造方法里,蕴含着初始化创建table数组的逻辑,源码和注释如下所示: -
从上面源码中我们可以看到,数组默认大小是16,设定的阈值为0.75的数组长度,并且根据传入的参数,创建了table数组中的第一个Entry元素对象。其中,size用来记录数组中存在的Entry元素的个数。 -
了解完createMap(t, value)方法之后,那么就把我们的视角切换到红框中的map.set(this, value)方法,这才是我们下面要分析的重点。 -
map.set(this, value)方法的相关源码和注释 -
关于set方法其实有两个,他们之间的关系就是——通过ThreadLocal的set方法来调用ThreadLocalMap的set方法。 -
在上面源码的四个红框中,我们下面会一一进行详细介绍。为了便于理解,用流程图描述,如下: -
通过上面的流程图,我们可以总结set方法有如下几个处理步骤: -
首先,通过入参key(即:ThreadLocal对象),计算应该插入table数组的下标。 -
如果该下标所在的位置是空闲的,那么就把新插入的值封装为Entry插入进去。 -
如果该下标所在的位置已经被别的Entry占据了,那么来进行如下判断: (1)、如果已存在的Entry的key值与我们的key值相同(即:是同一个ThreadLocal实例对象),那么我们只是将value值更新为方法入参的value即可。 (2)、如果key值不同,那么来判断,已存在的Entry是不是key==null(即:是一个“陈旧的”元素,那么我们进行替换操作) (3)、如果都不满足,那就往后遍历其他的Entry元素,直到满足上述条件为止,否则会一直循环。
3.3、nextIndex和prevIndex
- 我们先来看第一个红框中的方法nextIndex(i, len),其实通过该方法,我们还可以引出prevIndex(i, len)方法。源码和注释如下所示:
- 上图源码解释
- nextIndex就是从指定的下标i开始,向后获取下一个位置的下标值。
- preIndex就是从指定的下标i开始,前向获取上一个位置的下标值。
- 如果越界了怎么办呢?它们会采用循环查找法。即:获取队尾的下一个下标就会返回队首的下标;获取队首的上一个下标就会返回队尾的下标。如下所示:
3.4、开放地址法
3.4.1、开放地址法
- ThreadLocalMap并没有按照我们之前在学习HashMap的方式去解决哈希冲突,即:数组+链表。而它其实使用的是一种叫做“开放地址法”作为解决哈希冲突的一种方式。
- 开放地址法的基本思想就是:一旦发生了冲突,那么就去寻找下一个空的地址;那么只要表足够大,空的地址总能找到,并将记录插入进去。
3.4.2、ThreadLocalMap和HashMap的区别
3.4.3、链地址法和开放地址法的优缺点
-
开放地址法 (1)、容易产生堆积问题,不适于大规模的数据存储。 (2)、散列函数的设计对冲突会有很大的影响,插入时可能会出现多次冲突的现象。 (3)、删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂。 -
链地址法 (1)、处理冲突简单,且无堆积现象,平均查找长度短。 (2)、链表中的结点是动态申请的,适合构造表不能确定长度的情况。 (3)、删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。 (4)、指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间。
3.4.4、ThreadLocalMap采用开放地址法原因
- ThreadLocal往往存放的数据量不会特别大(而且key 是弱引用又会被垃圾回收,及时让数据量更小)。
- 采用开放地址法简单的结构会更节省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也比较低。
- 了解了开发地址法的原理之后继续看下面的源码
3.5、 replaceStaleEntry(key, value, i)
- 当发现待插入的位置上已经被其他Entry占用了,并且它的key值与我们不同(即:不是同一个ThreadLocal实例),那么,当这个已存在的Entry元素key==null的时候,逻辑上就走到了第二个红框里的方法——replaceStaleEntry(key, value, i),该方法是用来替换“陈旧的”Entry的。下面我们来看一下这个方法的代码和注释:
3.6、expungeStaleEntry(int staleSlot)
-
上面的replaceStaleEntry方法里面都调用了如下方法: -
方法的入参是slotToExpunge,它代表的含义是——我们上面“施工”范围内,最左侧的“陈旧”Entry下标位置。 -
其实也就是说,下面的清理工作,是以slotToExpunge作为起点,然后在“施工”范围内,向后一个个遍历处理“陈旧”Entry。 -
cleanSomeSlots这个方法在开篇的set方法的源码截图中用红框标注过,也算是我们见过面的方法了。但是expungeStaleEntry方法我们是第一次见到了,源码和注释如下所示: -
上图中源码解释如下: -
以slotToExpunge作为起点进行遍历,如果发现k==null(即:“陈旧”Entry),那么就赋值e.value=null,当前位置的Entry=null,这样gc就可以对其进行回收了。 -
面还会对每个k不为null的正常Entry进行重新的下标定位,目的就是让后面的元素往前面移动,因为开放地址寻找元素的时候,遇到null就停止寻找了,由于上面if代码中,k==null的时候已经设置entry为null了,不移动的话,后面的元素就访问不到了。 -
找到新的位置后,把Entry放到新的位置上,即:tab[h]=e;
3.7、 cleanSomeSlots(int i, int n)
- 该方法返回的是boolean值, 返回true:表示存在“陈旧”的Entry且已经被清除(但并不表示完全清除所有的“陈旧”Entry,只表示执行过这种操作)
- 由于上面的expungeStaleEntry方法,已经在“施工”范围内,清除了所有“陈旧的”Entry,并且由于在这个范围内,是不包含空位置的,所以可以顺利的把这个范围内的所有“陈旧”Entry清除掉。
- 那么cleanSomeSlots方法,则是以log2(n)的粒度,去清除一些“陈旧”Entry。
- 方法上的注释翻译如下,可以理解为是对于提升插入速度和table数组内“陈旧”Entry整理耗时的一种平衡处理方案:启发式扫描一些单元格以查找陈旧条目。当添加新元素或删除另一个陈旧元素时调用此方法。它执行对数扫描,作为不扫描(快速但保留垃圾)和扫描次数与元素数量成正比之间的平衡,这将找到所有垃圾但会导致某些插入花费 O(n) 时间。
- 源码和注释如下所示
3.8、rehash()
3.9、expungeStaleEntries()
- 该方法就是遍历table数组里的Entry,调用expungeStaleEntry方法(expungeStaleEntry详情上面介绍了)
- 源码和注释如下所示:
3.10、resize()
四、ThreadLocal 内存溢出问题
-
通过上面的分析,我们知道expungeStaleEntry() 方法是帮助垃圾回收的,根据源码,我们可以发现 get 和set 方法都可能触发清理方法expungeStaleEntry(),所以正常情况下是不会有内存溢出的。 -
但是如果我们没有调用get和set的时候就会可能面临着内存溢出。养成好习惯不再使用的时候调用remove(),加快垃圾回收,避免内存溢出。 -
就算我们没有调用get和set和remove方法,线程结束的时候,也就没有强引用再指向ThreadLocal中的ThreadLocalMap了,这样ThreadLocalMap和里面的元素也会被回收掉。 -
但是有一种危险是,如果线程是线程池的,在线程执行完代码的时候并没有结束,只是归还给线程池,这个时候ThreadLocalMap和里面的元素是不会回收掉的。
|