想必很多小伙伴们对ThreadLocal 并不陌生,ThreadLocal 叫做线程本地变量,也就是ThreadLocal为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部的副本变量。那么,我们使用ThreadLocal一定线程安全么?话不多说,先上结论:
如果threadlocal.get之后的副本,只在当前线程中使用,那么是线程安全的;如果对其他线程暴露,不一定是线程安全的。
为了演示下错误的使用方式,先看下如下代码(虽然小伙伴们都不会这样写代码 ^_^):
static class Container {
int num;
}
public void main(String[] args) throws InterruptedException {
ThreadLocal<Container> tl = new ThreadLocal<>();
tl.set(Container());
Container container = tl.get();
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
container.num++;
}
};
Thread t1 Thread(task);
Thread t2 Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(tl.get().num);
}
笔者的一次结果输出为:17581
结合代码,我们知道,在执行threadlcoal.get 获取到线程变量副本之后,不要让其他线程来访问它了,否则就是多线程操作同一个变量,可能造成线程安全问题。
除了上述讨论的ThreadLocal线程安全性问题之外,ThreadLocal如果使用不当,可能存在内存泄露问题。ThreadLocal变量是保存在Thread.threadLocals 中(ThreadLocalMap类型)以Entry类型保存的,其中Entry.key(也就是弱引用referent实际指向对象)为ThreadLocal变量,该变量为弱类型;Entry.value为实际set的value。
Entry extends WeakReference<ThreadLocal?>> {
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
虽然Entry.referent是弱类型,指向ThreadLocal变量,但是如果ThreadLocal变量本身引用不置为null的话,这里的Entry.referent指向对象是不会释放的。比如我们常用的定义方式:
类似于静态变量和对象属性这种引用,如果不将tls或tli设置为null,那么ThreadLocal变量无法释放(这不是废话么,人家可是强引用呀),此时的Entry.referent弱类型没啥卵用;只有在tls或tli为null时,Entry.referent弱类型就起作用了,在第一次GC时就会将Entry.referent弱类型指向的对象回收。
如果Entry.referent弱类型指向的对象回收了(没调用ThreadLocal.remove操作),Entry.value对象还在,并且Entry.value可是强引用的,此时就发生了内存泄露。这也就是ThreadLocal使用不当(没调用ThreadLocal.remove)时产生的内存泄漏问题。不过,伴随着其他ThreadLocal对象的set/get/remove 的进行,会清除一部分Entry.referent为null但是Entry.value不为null的对象的,也就是修复内存泄露问题,注意,这个只是清除部分这样的Entry,并不能保证一次就能清除全部这样的Entry,所以还是要遵循ThreadLocal.set,用完之后就remove。
讨论完了ThreadLocal的潜在问题之后,你是不是意犹未尽,想深入了解下ThreadLocal实现原理 ?OK,那就搬起小板凳,一起唠唠吧~
ps:如果小伙伴对ThreadLocal原理已经熟悉了,那么恭喜你,后面的内容可以不看了~
ThreadLocal实现原理
ThreadLocal变量主要有get/set/remove 三个操作,理解了这三个操作流程,基本上就理解了ThreadLocal实现原理。
get
get流程如下:
- 获取当前线程的threadLocals(map结构),从threadLocals中获取当前ThreadLocal变量对应的ThreadLocalMap.Entry(pair类型,包含了当前ThreadLocal变量及其对应的value),非空直接返回对应的value
- 为空时使用默认值(默认为null)构造ThreadLocalMap.Entry,放到当前线程的threadLocals中,下次再get时直接返回ThreadLocalMap.Entry对应的value即可
注意:线程的threadLocals是一个基于开放定址法实现的map结构。
set
- set操作就是将ThreadLocal变量的值put到当前线程的threadLocals中,ThreadLocal变量及其对应的值会构造成一个ThreadLocalMap.Entry放到threadLocals中。
- 因为线程的threadLocals是一个基于开放定址法实现的map结构,所以在出现hash冲突后会继续寻找下一个空位进行set操作。
- 因为是基于开放定址法,如果map中元素过多,会影响get和put性能,所以需要扩容,map的数组结构默认大小为
INITIAL_CAPACITY = 16 ,默认扩容阈值为threshold = INITIAL_CAPACITY * 2 / 3 ,扩容时按照成倍扩容。
remove
小结
从ThreadLocal的get/set 操作流程来看,ThreadLocal的value 是?Lazy Init(延迟初始化的) 。ThreadLocal为什么是延迟初始化,这个问题应该是容易理解的,原因是:在没有具体业务场景前提下,这样的做法避免内存浪费。
ThreadLocal变量默认放在基于开放定址法实现的map结构中,这种结构在hash冲突时会造成多次get/set 操作,理论上可以通过记录ThreadLocal变量set时的位置,这样下次直接通过该位置获取对应value即可,可以参考netty的FastThreadLocal ,它的实现思路就是这样的,提高了set/get的效率。
?
最后来一张ThreadLocal的整体图:

参考资料:
1、https://luoxn28.github.io/2019/04/27/ni-de-threadlocal-yi-ding-xian-cheng-an-quan-ma/ (编辑:北几岛)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!
|