抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

ThreadLocal源码解析02

上一节我们详细解析了set方法,现在我们来解析get方法

ThreadLocal.get 方法

ThreadLocal.get 方法相对来说简单

我们先来看下get的流程图

下面我们解析代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//获取ThreadLocal 中的值
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//同set方法类似获取对应线程中的ThreadLocalMap实例
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null) {
//从Map中根据ThreadLocal获取entry
ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
//如果Entry 不为空 则返回
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
//为空返回初始化值
return setInitialValue();
}

/**
* 初始化设值的方法,可以被子类覆盖。
*/
protected T initialValue() {
return null;
}

/**
* 设置初始化的方法
* @return
*/
private T setInitialValue() {
//获取初始化值,默认为null(如果没有子类进行覆盖)
T value = initialValue();
Thread t = Thread.currentThread();
//同set方法类似获取对应线程中的ThreadLocalMap实例
ThreadLocalMap map = getMap(t);
//不为空不用再初始化,直接调用set操作设值
if (map != null)
map.set(this, value);
else {
//第一次初始化,createMap在上面介绍set()的时候有介绍过。
createMap(t, value);
}
//返回初始化后的值
return value;
}

我们发现首先从当前线程中获取ThreadLocalMap 然后从map中根据ThreadLocal获取entry,entry不为空则返回value,否则调用setInitialValue设置初始值并返回。

我们深入到 map.getEntry(this);方法

ThreadLocalMap.getEntry 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 根据 ThreadLocal 获取获取entry
* @param key 当前的threadLocal
* @return
*/
private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {
//计算 table 中需要映射的下标
int i = key.threadLocalHashCode & (table.length - 1);
//从table中获取entry
ThreadLocal.ThreadLocalMap.Entry e = table[i];
//如果entry 不为空且entry中的key和传入的key匹配则返回entry
if (e != null && e.get() == key)
return e;
else {
//发生了hash冲突,当前的entry为空或者key和传入的key不一致
return getEntryAfterMiss(key, i, e);
}
}

getEntryAfterMiss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 通过直接计算出来的key找不到对于的value的时候适用这个方法.
*/
private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal<?> key, int i, ThreadLocal.ThreadLocalMap.Entry e) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;

while (e != null) {
ThreadLocal<?> k = e.get();
//如果当前的key和传入的key匹配则返回
if (k == key)
return e;
if (k == null)
//清除无效的entry
expungeStaleEntry(i);
else {
//基于线性探测法向后扫描
i = nextIndex(i, len);
}
e = tab[i];
}
//找不到返回null
return null;
}

到这里 get方法以及完结了。

ThreadLocal.remove 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void remove() {
//同set方法类似获取对应线程中的ThreadLocalMap实例
ThreadLocal.ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
//调用ThreadLocalMap的remove方法进行清除entry
m.remove(this);
}
}

/**
* 根据ThreadLocal 删除entry
* @param key 当前的ThreadLocal
*/
private void remove(ThreadLocal<?> key) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
//计算 table 中需要映射的下标
int i = key.threadLocalHashCode & (len - 1);
//进行线性探测,查找正确的key
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//调用weakrefrence的clear()清除引用
e.clear();
//连续段清除
expungeStaleEntry(i);
return;
}
}
}

​ remove()在有上面了解后可以说极为简单了,就是找到对应的table[],调用weakrefrence的clear()清除引用,然后再调用expungeStaleEntry()进行清除。

ThreadLocal 防止hash冲突的

到这里整个threadLocal 基本介绍完成了,但是还少一块,如何处理hash冲突的

生成映射下标

1
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

这一段代码和 求模的效果类似,根据hash生成一个0-(INITIAL_CAPACITY-1)之间的数

我们发现计算冲突与threadLocalHashCode 和 INITIAL_CAPACITY有关

INITIAL_CAPACITY值的设置

例如 :

10 0001 0111 1101

00 0000 0000 1111 & (INITIAL_CAPACITY-1) 即 15


00 0000 0000 1101 13

所以这种算法只对后半段的数据敏感 如果是其他值 后面可能包含0 例如 0011 这样只有两位参与了运算,重复率就增加了,如果下面的值全是1 就更加平均了,什么时候全是1呢 就是 2的N次幂-1

例如

2^4=16=10000B

16-1=15= 01111B

所以只有当INITIAL_CAPACITY值时2的n次幂的时候才对hash的数据敏感,因为是与运算,只利用了hahs二进制的后半段。

threadLocalHashCode 值的设置

我们看下threadLocalHashCode 的声明

1
2
3
4
5
6
private final int threadLocalHashCode = nextHashCode();
//返回下一个hashCode
private static int nextHashCode() {
//通过CAS的方式进行获取并且相加
return nextHashCode.getAndAdd(HASH_INCREMENT);
}

这里面有一个神奇的数字 HASH_INCREMENT

他的声明是

1
private static final int HASH_INCREMENT = 0x61c88647;
神奇的数字

既然ThreadLocal用map就避免不了冲突的产生

这里碰撞其实有两种类型

  1. 只有一个ThreadLocal实例的时候(上面推荐的做法),当向thread-local变量中设置多个值的时产生的碰撞,碰撞解决是通过开放定址法, 且是线性探测(linear-probe)
  2. 多个ThreadLocal实例的时候,最极端的是每个线程都new一个ThreadLocal实例,此时利用特殊的哈希码0x61c88647大大降低碰撞的几率, 同时利用开放定址法处理碰撞

注意 0x61c88647的利用主要是为了多个ThreadLocal实例的情况下用的

注意实例变量threadLocalHashCode, 每当创建ThreadLocal实例时这个值都会累加 0x61c88647,为了让哈希码能均匀的分布在2的N次方的数组里, 即 Entry[] table的大小必须是2的N次方

我们看下table的定义

1
2
3
4
5
6
7
/**
* The table, resized as necessary.
* 该表根据需要调整大小。
* table.length MUST always be a power of two.
* table.length必须始终是2的幂。
*/
private Entry[] table;

key.threadLocalHashCode & (len-1)这么用是什么意思? 我们上面定义了table数组的长度是16 =2^4

ThreadLocalMap 中Entry[] table的大小必须是2的N次方呀(len = 2^N),那 len-1 的二进制表示就是低位连续的N个1, 那 key.threadLocalHashCode & (len-1) 的值就是 threadLocalHashCode 的低N位, 这样就能均匀的产生均匀的分布? 我们做个实验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//神奇的数字
private static final int HASH_INCREMENT = 0x61c88647;
//模拟table大小 16 2^4
private static final int tableSize = 1 << 4;

public static void main(String[] args) {
getHashIndex(1000);
}

public static void getHashIndex(int num) {
Map<Integer, Integer> map = new HashMap<>();
int nextHashCode = HASH_INCREMENT;
for (int i = 0; i < num; i++) {
//每次进行*2 计算
nextHashCode += HASH_INCREMENT;
//计算映射的小标
int index = nextHashCode & (tableSize - 1);
//用Map进行统计
Integer count = map.computeIfAbsent(index, x -> 0);
map.put(index, ++count);
}
System.out.println(map);
}

输出

1
{0=62, 1=63, 2=62, 3=63, 4=62, 5=63, 6=62, 7=62, 8=63, 9=62, 10=63, 11=62, 12=63, 13=62, 14=63, 15=63}

我们发现数据分布的惊人的平均,比我们写的随机数更平均。

总结

Hash冲突怎么解决

​ 和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

内存泄露问题

​ threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露. 最好的做法是将调用threadlocal的remove方法.

​ 因为ThreadLocal本身又清理机制,调用,get,set,remove等方法时会触发自动清理机制,清理掉key为空的主句,但是不是实时的,会有延后,在没有调用get,set,remove方法时,过期的entry时内存泄漏状态,推荐不适用了调用remove方法。

hash散列算法

​ 在实际使用中,不同的输入可能会散列成相同的输出,这时也就产生了冲突。通过上文提到的 HASH_INCREMENT 再借助一定的算法,就可以将哈希码能均匀的分布在 2 的 N 次方的数组里,保证了散列表的离散度,从而降低了冲突几率,使用 nextHashCode & (tableSize - 1);这种方式进行下标映射性能更高,使用用HASH_INCREMENT 0x61c88647 这个神奇的数字让数据分布的更平均。

评论