ThreadLocal
以数据库连接为例,如果多个线程共享一个连接,有可能一个线程在对数据库进行操作,另一个线程调用了closeConnection操作;如果在每一个线程都new一个连接对象,如果开启关闭数据库操作频繁,会影响到服务器压力,并且影响程序执行性能。
=> ThreadLocal 内部维护一个 ThreadLocalMap 类,保存的是 Entry<Thread K, Object V> 数组,K是线程,V是值,这样每一个线程无论在哪里调用,都会拿到自己线程的值
每个Thread对象中都持有一个ThreadLocalMap的成员变量。每个ThreadLocalMap内部又维护了N个Entry节点,也就是Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值
ThreadLocalMap的引用是在Thread里的,所以它里面的Entry数组存放的是一个线程里new出来的多个ThreadLocal对象
Thread维护了ThreadLocalMap,而ThreadLocalMap里维护了Entry,而Entry里存的是以ThreadLocal为key,传入的值为value的键值对。
1 | // java.lang.Thread类里持有ThreadLocalMap的引用 |
主要方法:
initialValue:初始化。在get方法里懒加载的。
- 通常,每个线程最多调用一次此方法。但是如果已经调用了remove(),然后再次调用get()的话,则可以再次触发initialValue。
- 如果要重写的话一般建议采取匿名内部类的方式重写此方法,否则默认返回的是null。
1
2
3
4
5
6
7
8
9public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
// Java8的高逼格写法
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));get:得到这个线程对应的value。如果调用get之前没set过,则get内部会执行initialValue方法进行初始化。
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/**
* 获取当前线程下的entry里的value值。
* 先获取当前线程下的ThreadLocalMap,
* 然后以当前ThreadLocal为key取出map中的value
*/
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程对应的ThreadLocalMap对象。
ThreadLocalMap map = getMap(t);
// 若获取到了。则获取此ThreadLocalMap下的entry对象,若entry也获取到了,那么直接获取entry对应的value返回即可。
if (map != null) {
// 获取此ThreadLocalMap下的entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
// 若entry也获取到了
if (e != null) {
@SuppressWarnings("unchecked")
// 直接获取entry对应的value返回。
T result = (T)e.value;
return result;
}
}
// 若没获取到ThreadLocalMap或没获取到Entry,则设置初始值。 懒加载方式
return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
// 如果e=null,说明出现碰撞问题,通过开放寻址方法来继续寻找
return getEntryAfterMiss(key, i, e);
}
// 通过布长+1或-1来寻找下一个相邻的位置
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}set:为这个线程设置一个新值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/**
* 设置当前线程的线程局部变量的值
* 实际上ThreadLocal的值是放入了当前线程的一个ThreadLocalMap实例中,所以只能在本线程中访问。
*/
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程对应的ThreadLocalMap实例,注意这里是将t传进去了,t是当前线程,就是说ThreadLocalMap是在线程里持有的引用。
ThreadLocalMap map = getMap(t);
// 若当前线程有对应的ThreadLocalMap实例,则将当前ThreadLocal对象作为key,value做为值存到ThreadLocalMap的entry里。
if (map != null)
map.set(this, value);
else
// 若当前线程没有对应的ThreadLocalMap实例,则创建ThreadLocalMap,并将此线程与之绑定
createMap(t, value);
}remove:ThreadLocalMap键为弱引用,删除这个线程对应的值,防止内存泄露的最佳手段。
碰撞解决与神奇的 0x61c88647(十进制:1640531527)
两种碰撞类型
- 只有一个ThreadLocal实例的时候(上面推荐的做法),当向thread-local变量中设置多个值的时产生的碰撞,碰撞解决是通过开放定址法, 且是线性探测(linear-probe)
- 多个ThreadLocal实例的时候,最极端的是每个线程都new一个ThreadLocal实例,此时利用特殊的哈希码0x61c88647大大降低碰撞的几率, 同时利用开放定址法处理碰撞
显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。
所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。
key.threadLocalHashCode ==> AtomicInteger.getAndAdd(HASH_INCREMENT) 其中 HASH_INCREMENT = 0x61c88647,这个值是 为了让哈希码能均匀的分布在2的N次方的数组里
This number represents the golden ratio (sqrt(5)-1) times two to the power of 31 ((sqrt(5)-1) * (2^31)). The result is then a golden number, either 2654435769 or -1640531527.
魔数0x61c88647的与斐波那契散列有关,0x61c88647对应的十进制为1640531527。斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。换句话说(1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是1640531527也就是0x61c88647。通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。