ThreadLocal
什么是ThreadLocal
ThreadLoacal
类可以为每个线程保存一份独有的变量
,该变量对于每个线程都是独占的。实现原理为每个Thread
类中包含一个ThreadHashMap
,key
为变量的name
,value
为变量的值
。
在日常使用中,我们可以通过set(value)
方法设置值,然后通过get()
方法获取值,示例代码如下:
我们通过线程池执行了五个线程,每个线程都有一份自己独有的id变量
,实现了线程间的数据隔离。
@Slf4j
public class ThreadLocalTest {
// ThreadLocal 变量,每个线程都有自己的副本
private static final ThreadLocal<Integer> id = new ThreadLocal<>();
@Test
public void testThreadLocal() {
// 创建线程池
ExecutorService executorService = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 5; i ++) {
int finalI = i;
executorService.execute(() -> {
// 设置ThreadLocal 变量
id.set(finalI);
// 获取ThreadLocal变量
log.info("The id of thread {} is {}", Thread.currentThread().getName(), id.get());
});
}
} finally {
id.remove();
executorService.shutdown();
}
}
}
传统的
HashMap
是链地址法,发生碰撞放入桶内;ThreadLocalMap
是开放地址法,发生冲突放到后一位
实现原理
那如何实现这种线程间的数据隔离呢?一种容易想到的方法是在Map
中进行存储,每个ThreadLocal
变量内维护一个Map
,其中key
为每个线程的ID
,value
为对应的值。这样当调用get()
方法时,会通过hash算法
找到对应线程的值。
jdk
也确实是这么做的,只不过存放元素Map
的实现不是HashMap
,而是ThreadLocalMap
,解决冲突的做法是开放寻址法
,即出现冲突就往下一个临近位置找,直到找到空位置。
早期的实现中,多个线程会共享一个ThreadLocalMap
,这样随着线程数的增加,map
就需要扩容,会消耗较多的资源;
后来,每个线程都有自己的一个ThreadLocalMap
,各自保存自己的局部变量,key
为变量名,value
为变量值。
因为局部变量很少,所以一般不需要扩容。
内存泄漏
下面这张图是ThreadLocal
对应的引用关系,可以看到,在新的设计下,ThreadLocalMap
存在于Thread
中,是和Thread
的生命周期一致的。
ThreadLocalMap
的key
为ThreadLocal
对象,value
是对应的值,需要注意的是ThreadHashMap
的key
会使用弱引用,这主要是为了当ThreadLocal
为null
后,除了ThreadHashMap
持有的弱引用外,没有别的强引用,ThreadLocal
对象可以被GC回收
。
但是这样就导致ThreadHashMap
中对应的key
为null
,其value
引用的对象不会被GC回收
,从而出现了内存泄漏。
所以在使用完ThreadLocal
变量后,尽量使用remove()
方法进行清理,这样会把key
为null
的键值对删除。
再回到上面的示例,上面的示例我们使用final修饰了ThreadLocal
,保证其不会被修改,这样做会导致ThreadLocal
永远存在强引用,不会被释放,容易发生内存泄露,所以需要我们显示调用remove()
方法进行清理。
参考链接
ThreadLocal的内存泄露?什么原因?如何避免?