关键词: 空间换时间、弱引用、开放地址法(线性探测,二次探测,双重探测)
简介
ThreadLocal 是一个线程的本地变量,也就意味着这个变量是线程独有的,是不能与其他线程共享的,这样就可以避免资源竞争带来的多线程的问题,这种解决多线程的安全问题和lock是有本质的区别的。(这里的lock 指通过synchronized 或者Lock 等实现的锁)
Lock
- 资源是多个线程共享的,所以访问的时候需要加锁
- 通过时间换空间的做法
ThreadLocal
- 每个线程都有一个副本,是不需要加锁的
- 通过空间换时间的做法
使用
ThreadLocal 的使用是非常简单的,看下面的代码
public class Test {
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
// 设置值
threadLocal.set("hello world");
// 获取值
System.out.println(threadLocal.get());
}
}
源码分析
分析源码之前先画一下ThreadLocal ,ThreadLocalMap 和Thread 的关系
java.lang.ThreadLocal#set 方法
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread();
// 获取线程绑定的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 第一次设置值的时候创建
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
...
}
createMap 方法只是在第一次设置值的时候创建一个ThreadLocalMap 赋值给Thread 对象的threadLocals 属性进行绑定,以后就可以直接通过这个属性获取到值了。从这里可以看出,为什么说ThreadLocal 是线程本地变量来的了。
值真正是放在ThreadLocalMap 中存取的,ThreadLocalMap 内部类有一个Entry 类,key是ThreadLocal 对象,value 就是你要存放的值,上面的代码value 存放的就是hello word。ThreadLocalMap 和HashMap的功能类似,但是实现上却有很大的不同:
HashMap
- 数据结构是数组+链表
- 通过链地址法解决hash冲突
- Entry内部类的引用都是强引用
ThreadLocalMap
- 数据结构仅仅是数组
- 通过开放地址法来解决hash 冲突的问题
- Entry内部类中的key是弱引用,value是强引用
链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。列如对于关键字集合{12,67,56,16,25,37, 22,29,15,47,48,34},我们用前面同样的12为除数,进行除留余数法:
链地址法优缺点:
- 处理冲突简单,且无堆积现象,平均查找长度短。
- 链表中的结点是动态申请的,适合构造表不能确定长度的情况。
- 删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
- 指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间。
开放地址法
这种方法的基本思想是一旦发生了冲突,就去寻找下一个空的散列地址(这非常重要,源码都是根据这个特性,必须理解这里才能往下走),只要散列表足够大,空的散列地址总能找到,并将记录存入。
比如说,我们的关键字集合为{12,33,4,5,15,25},表长为10。 我们用散列函数f(key) = key mod l0。 当计算前S个数{12,33,4,5}时,都是没有冲突的散列地址,直接存入(蓝色代表为空的,可以存放数据):
计算key = 15时,发现f(15) = 5,此时就与5所在的位置冲突。于是我们应用上面的公式f(15) = (f(15)+1) mod 10 =6。于是将15存入下标为6的位置。这其实就是房子被人买了于是买下一间的作法:
开放地址法优缺点:
- 容易产生堆积问题,不适于大规模的数据存储。
- 散列函数的设计对冲突会有很大的影响,插入时可能会出现多次冲突的现象。
- 删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂。
魔数0x61c88647
- 生成hash code间隙为这个魔数,可以让生成出来的值或者说ThreadLocal的ID较为均匀地分布在2的幂大小的数组中。
public class ThreadLocal {
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}
- 可以看出,它是在上一个被构造出的ThreadLocal的ID/threadLocalHashCode的基础上加上一个魔数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的幂取模,得到的结果分布很均匀。
- ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。为了优化效率。
ThreadLocalMap 采用开放地址法原因
- ThreadLocal 往往存放的数据量不会特别大(而且key 是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低
java.lang.ThreadLocal.ThreadLocalMap#set 方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算数组的下标
int i = key.threadLocalHashCode & (len-1);
// 循环的结束条件是 e!= null
// 先通过hash找到数组下标,然后寻找相等的ThreadLocal对象
// 找不到就往下一个index找,有两种可能会退出这个循环
// 1. 找到了相同ThreadLocal对象
// 2. 一直往数组下一个下标查询,直到下一个下标对应的是null 跳出
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果找到直接设置value值,并返回
if (k == key) {
e.value = value;
return;
}
// k==null && e!=null 说明key被垃圾回收了,这里涉及到弱引用
if (k == null) {
// 被回收的话就需要替换掉过期的值,把新的值放在这里返回
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocal内存泄漏?
- key-弱引用、value-强引用 当jvm内存不足时,会回收key,如果此时value还有引用就不会被回收。所以有内存泄漏的危险。
如何避免ThreadLoacl内存泄漏
-
get、set方法都可能触发清理方法,所以在正常使用来讲,是不会发生内存泄漏。为了避免发生内存泄漏,养成好的习惯在使用完后调用remove。
-
退一步来讲,线程结束时,也就没有强引用指向ThreadLocalMap了,这样ThreadLocalMap里的元素Entry(key、value)会全部被回收掉。