作用
将资源副本保存各个线程中,不同线程间不会相互干扰,这种变量在线程的生命周期中起作用,降低了每个线程内公共变量在不同组建/模块间的传递复杂性。总而言之,在于三个点:
- 线程并发:在多线程的场景下,
ThreadLocal
有大用途;如果仅想在单线程下使用也可以 - 数据传递:可以在同一线程里,不同组件/模块间传递公共变量
- 线程隔离:每个变量都是线程独有的,不会互相影响
与Synchronized的对比
在某些场景下,使用 synchronized
和 ThreadLocal
作用是一样的,但是它们之间有以下这样的区别:
synchrnoized | ThreadLocal | |
---|---|---|
原理 | 采用“以时间换空间”的方式,只提供了一个变量,让多个线程排队去修改/读取 | 采用“以空间换时间”的方式,为每一个线程都提供了一份变量副本,从而实现不同线程间的隔离 |
侧重点 | 多个线程访问资源的串行化 | 让各个线程里的资源独立,互不干扰 |
所以,可以使用 ThreadLocal
的场景下,尽量去使用 ThreadLocal
,因为不需要上锁,所以性能好一些。
总而言之, ThreadLocal
采用了资源隔离方法,各个线程的资源不共享,那么就不存在并发问题了。
内部结构
点击查看【processon】
两者相较而言, JDK8
的设计有以下几个好处:
- 不需要考虑多个新线程往
ThreadLocalMap
里设置数据的并发问题- 在
JDK8
以前,仍然会有这样一种情况,多个线程对ThreadLocalMap
同时修改的情况 - 在
JDK8
改进后,ThreadLocalMap
已经是线程私有的东西,不存在并发问题
- 在
- 每当
Thread
销毁的时候ThreadLocalMap
也会随之销毁,减少内存的开销- 在
JDK8
以前,线程销毁后,ThreadLocalMap
里保存的对应线程的资源不会立刻释放掉 - 在
JDK8
改进后,ThreadLocalMap
集成在Thread
中,当Thread
销毁,ThreadLocalMap
随之销毁
- 在
ThreadLocal
set(T value)
:设置值public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取Thread.threadLocals属性
ThreadLocalMap map = getMap(t);
// 如果map不为空,即已经存在了ThreadLocalMap
if (map != null)
// 单纯的设置value到对应的ThreadLocalMap里就行了
map.set(this, value);
else
// 否则创建ThreadLocalMap
createMap(t, value);
}
-----------------------------------我是分割线-------------------------------------------
void createMap(Thread t, T firstValue) {
// 如果当前的Thread没有ThreadLocalMap,就创建一个
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
get()
:获取值public T get() { // 获取当前线程 Thread t = Thread.currentThread(); // 获取Thread.threadLocals属性 ThreadLocalMap map = getMap(t); // 如果存在map if (map != null) { // 获取map里,当前ThreadLocal对应的值 ThreadLocalMap.Entry e = map.getEntry(this); // 如果entry不为空,就返回 if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 如果e为空 或者 map为空,就初始化当前的ThreadLocalMap,并设置初始值 return setInitialValue(); } -----------------------------------我是分割线------------------------------------------- private T setInitialValue() { // 让子类实现的方法,如果不实现,默认是null T value = initialValue(); Thread t = Thread.currentThread(); // 获取当前Thread的ThreadLocalMap ThreadLocalMap map = getMap(t); // 如果已经存在ThreadLocalMap,则设置默认值 if (map != null) map.set(this, value); else // 否则创建ThreadLocalMap,并创建默认值 createMap(t, value); return value; }
东西比较简单,就是要搞清楚哪个设置到哪个里面去。首先
ThreadLocalMap
是承载ThreadLocal
对象和和对应值的容器;每个ThreadLocal
对应着一个特定的值,所以要想保存多个值,就是创建多个不同的ThreadLocal
即可。ThreadLocalMap
Entry
在
ThreadLocalMap
中使用Entry
来作为键值对元素,而Entry
的键只能是ThreadLocal
。另外Entry
通过继承WeakReference
将ThreadLocal
对象的生命周期和Thread
的生命周期解绑,当没有强引用指向 ThreadLocal 变量时,它可被回收。/** * 继承自WeakReference */ static class Entry extends WeakReference<ThreadLocal<?>> { // 该ThreadLocal对应的值 Object value; Entry(ThreadLocal<?> k, Object v) { // ThreadLocal作为 WeakReference 保留下来 super(k); // 保存值 value = v; } }
注意,这里仅仅是
Key
(ThreadLocal
)可以被回收,但是整个Entry
键值对元素还是存在内存泄漏的可能。
实践
ThreadLocal
用起来感觉挺不直观的,所以这里再记录一下:
@Test
public void testThreadBasic(){
// 创建一个ThreadLocal对象(这货其实是一个Key)
final ThreadLocal<SingletonBean2> myThreadLocal = new ThreadLocal<>();
// 在set()过程中,会通过Thread.cuurentThread()获取当前线程
// 然后给Thread.threadLocals添加(put)一个 ThreadLocal -> Value 的映射
// 拿下面这个例子来说就是 给当前线程(main线程)的threadLocals(Map)添加了一个
// ThreadLocal -> Value的映射
myThreadLocal.set(singletonBean1);
// 这里新建了一个线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 尝试通过相同的ThreadLocal对象获取值
// 注意一定得是相同的ThreadLocal对象(当然,这里是获取不到值的,因为映射保存在
// main线程中)
System.out.println(myThreadLocal.get());
}
});
thread.start();
}
拓展
InheritableThreadLocal
这里提一个比较有趣的东西,今天学习 Thread
的时候看到的~~就是 InheritableThreadLocal
这货了,这货继承了 ThreadLocal
,然后覆盖了以下几个方法:
方法原型 | 方法释义 |
---|---|
createMap(Thread, T) | 给 Thread 创建一个 ThreadLocalMap ,并添加一个this -> value 的映射 |
getMap(Thread) | 用来获取 Thread.inheritableThreadLocals 属性 |
childValue(T) | 用来获取子线程应该继承到的值,可以通过继承 InheritableThreadLocal 重新定义每个子线程从父线程继承过来的值的样子 |
简单来说,以前通过 ThreadLocal#set()
设置值是设置给 CurrentThread.threadLocals
属性;现在若通过 InheritableThreadLocal#set()
设置值,则是设置给当前线程 CurrentThread.inheritableThreadLocals
属性的,而且该属性是可以被子线程继承的(内置逻辑是有线程组就会开启继承,不是线程组就不开启)。我们来看看它的代码演示:
public void main(String[] args){
// 可以继承的Bean
SingletonBean2 canBeInheritableBean = new SingletonBean2("Hello");
// 不可以继承的Bean
SingletonBean2 notBeInheritableBean = new SingletonBean2("World");
// InheritableThreadLocal
final InheritableThreadLocal<SingletonBean2> inheritableThreadLocal = new InheritableThreadLocal<>();
// 是设置给当前线程的inheritableThreadLocals属性里(一样是ThreadLocalMap)
inheritableThreadLocal.set(canBeInheritableBean);
// 创建ThreadLocal
final ThreadLocal<SingletonBean2> myThreadLocal = new ThreadLocal<>();
// 是设置给当前线程的threadLocals属性里
myThreadLocal.set(notBeInheritableBean);
// 创建一个子线程,通过源码点进去可以发现它开启了继承功能
// 所以当前线程(main线程)会将inheritableThreadLocals设置到子线程里面去
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 输出为null,因为这是一个新线程,它没有设置ThreadLocal
System.out.println(myThreadLocal.get());
// 输出为从父线程继承过来的值,即canBeInheritableBean
System.out.println(inheritableThreadLocal.get());
}
});
thread.start();
}
最后输出为:
null
SingletonBean2{title='Hello'}
除此之外,我们可以对 InheritableThreadLocal
进行拓展,重写赋值给子类的值(中间层):
static class MyInheritableThreadLocal extends InheritableThreadLocal<SingletonBean2>{
@Override
protected SingletonBean2 childValue(SingletonBean2 parentValue) {
return new SingletonBean2(){
@Override
public String toString() {
return "我重新定义了SingletonBean2的toString,你也可以做其他自定义的修改~";
}
};
}
}
然后对我们前面的测试代码(第七行)稍作修改:
public void main(String[] args){
// 省略
...
// +++改成如下这样的代码
final MyInheritableThreadLocal inheritableThreadLocal = new MyInheritableThreadLocal();
// 省略
...
}
最后输出:
null
我重新定义了SingletonBean2的toString,你也可以做其他自定义的修改~
总结
是否存在内存泄漏
是的,仍然会有内存泄漏。示意图如下所示:
点击查看【processon】
即便 key
是弱引用,在GC时 key
能够被回收;但是 Entry
仍然会被 Map
持有着,所以对应 Entry
内的 Value
仍然释放不掉。在这种情况下,如果 Thread
还是线程池持有的,又没有手动释放,就会产生内存泄漏了。
总而言之,当 Thread
不是用完即毁的那种,而且又没有手动移除 **Entry
,那么就会造成内存泄漏**。
为什么使用弱引用作为Key
其实无论是弱引用还是强引用,都会存在内存泄漏的问题。那使用强引用作为 Key
行吗?
- 强引用下
Key
是百分百无法移除了 - 弱引用下外部的线程一旦不使用这个
Key
了,下次gc
的时候,这个Key
就会被回收,该Reference
就是null
值。
在弱引用这种情况下可以找到**不再被外部使用**的 Key
,间接通过其他手段实现 Entry
的清理;比如通过 set()
、 getEntry()
等方法,一旦发现 key == null
就进行清理工作。
使用WeakHashMap不行吗
可以是可以,但是太复杂了。 ThreadLocal
就只需要 get
、 set
、 remove
而已,所以自己实现一个简单的达到目的 ThreadLocalMap
即可。
解决内存泄漏
- 每次用完手动移除
Entry
- 线程用后即毁