文章目录
零、前言
时间 | 内容 |
---|---|
2020.11.4 | 推荐阅读 https://mp.weixin.qq.com/s/5cogR2OQyIndB95W1Q77HA。里面讲的比本文好得多 |
下面贴一部分该文章的内容,供自己学习:
1. ThreadLocal 内存模型
图中左边是栈,右边是堆。线程的一些局部变量和引用使用的内存属于Stack(栈)区,而普通的对象是存储在Heap(堆)区。
线程运行时,我们定义的TheadLocal对象被初始化,存储在Heap,同时线程运行的栈区保存了指向该实例的引用,也就是图中的ThreadLocalRef。
当ThreadLocal的set/get被调用时,虚拟机会根据当前线程的引用也就是CurrentThreadRef找到其对应在堆区的实例,然后查看其对用的TheadLocalMap实例是否被创建,如果没有,则创建并初始化。
Map实例化之后,也就拿到了该ThreadLocalMap的句柄,那么就可以将当前ThreadLocal对象作为key,进行存取操作。
图中的虚线,表示key对应ThreadLocal实例的引用是个弱引用。
2. ThreadLocal 为什么要用弱引用
从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得思考:为什么ThreadLocalMap使用弱引用而不是强引用?
翻看官网文档的说法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了处理非常大和长期的用途,哈希表条目使用weakreference作为键。
分两种情况讨论:
-
key 使用强引用
引用ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
-
key 使用弱引
引用ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal被清理后key为null,对应的value在下一次ThreadLocalMap调用set、get、remove的时候可能会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
2. ThreadLocal适用场景
- 当需要存储线程私有变量的时候。
- 当需要实现线程安全的变量时。
- 当需要减少线程资源竞争的时候。
一、ThreadLocal 简介
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
官方注释:
该类提供线程局部变量。这些变量不同于它们的普通副本,因为每个访问一个变量的线程(通过其(ecode get}或(ecode set}方法)都有自己的、独立初始化的变量副本。 ThreadLocal实例通常是希望将状态与线程(例如,用户ID或事务ID)关联的类中的私有静态字段
简单来说:
ThreadLocal为每个线程提供了一个变量副本。每个线程可以使用并修改这个副本,相互之间并不影响。
二、ThreadLocal 的使用
需要注意: 无论是官方还个人,都建议你使用 private static 来修饰 ThreaLocal 变量
1. 如下代码,注释比较清晰,不再赘述。
/**
* @Data: 2019/11/6
* @Des: ThreadLocal 使用Demo
* 执行结果:
* t1获取到了threadLocal 中的值 : 100
* t1修改到了threadLocal 中的值 : 999
* t2获取到了threadLocal 中的值 : 100
*
*/
public class ThreadLocalDemo {
public static void main(String[] args) {
// 0. 设置ThreadLoca 初始值为100
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 100);
// 1. 创建线程t1
Thread t1 = new Thread(() -> {
String threadName = Thread.currentThread().getName();
// 1.1 获取 ThreadLocal 中的初始值并打印
System.out.println(threadName + "获取到了threadLocal 中的值 : " + threadLocal.get());
// 1.2 重新赋值为999
threadLocal.set(999);
// 1.3 打印赋值后的结果。
System.out.println(threadName + "修改到了threadLocal 中的值 : " + threadLocal.get());
}, "t1");
// 2. 创建线程t2
Thread t2 = new Thread(() -> {
try {
// 1.1 睡眠1s是为了让线程1跑完,设值结束,更好论证
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String threadName = Thread.currentThread().getName();
// 2.2 打印出当前在线程2中ThreadLocal中的值
System.out.println(threadName + "获取到了threadLocal 中的值 : " + threadLocal.get());
}, "t2");
// 3. 启动两个线程
t1.start();
t2.start();
}
}
可以看到,当线程t1获取到ThreadLocal 中变量值时是100,随机将值修改了999,然后t1再次获取变量值变成了999。证明t1线程的修改是成功的。然后t2线程获取到 ThreadLocal 中的值,还为100。即可证明,ThreadLocal 中的变量是以线程为作用域。
2. 下面的代码可以更加详细的证明这点,代码比较清晰,就不做解释。
/**
* @Data: 2019/11/6
* @Des: ThreadLocal 使用Demo
* 执行结果
* t1获取到了threadLocal 中的值 : 100
* t1修改到了threadLocal 中的值 : 999
* t2获取到了threadLocal 中的值 : 100
* t2又获取到了threadLocal 中的值 : 666
* t1获取到了threadLocal 中的值 : 999
*/
public class ThreadLocalDemo {
public static void main(String[] args) {
// 0. 设置ThreadLoca 初始值为100
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 100);
// 1. 创建线程t1
Thread t1 = new Thread(() -> {
String threadName = Thread.currentThread().getName();
// 1.1 获取 ThreadLocal 中的初始值并打印
System.out.println(threadName + "获取到了threadLocal 中的值 : " + threadLocal.get());
// 1.2 重新赋值为999
threadLocal.set(999);
// 1.3 打印赋值后的结果。
System.out.println(threadName + "修改到了threadLocal 中的值 : " + threadLocal.get());
try {
// 1.4 睡眠2s是为了让线程2跑完,设值结束,更好论证
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 1.5 获取变量值
System.out.println(threadName + "获取到了threadLocal 中的值 : " + threadLocal.get());
}, "t1");
// 2. 创建线程t2
Thread t2 = new Thread(() -> {
try {
// 1.1 睡眠1s是为了让线程1跑完,设值结束,更好论证
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String threadName = Thread.currentThread().getName();
// 2.2 打印出当前在线程2中ThreadLocal中的值
System.out.println(threadName + "获取到了threadLocal 中的值 : " + threadLocal.get());
// 2.3 设置变量值
threadLocal.set(666);
// 2.4 获取变量值
System.out.println(threadName + "又获取到了threadLocal 中的值 : " + threadLocal.get());
}, "t2");
// 3. 启动两个线程
t1.start();
t2.start();
}
}
三、ThreadLocal 实现原理
ThreadLocal 的 原理其实很简单,关键的就是get和set方法。
1. ThreadLocal#get()
-
首先我们需要知道Thread 类中是有一个
threadLocals
对象,他的类型是ThreadLocal.ThreadLocalMap
,
与此线程相关的ThreadLocal
值。这个映射由ThreadLocal
类维护。简单来说, Thread类中有一个ThreadLocal.ThreadLocalMap
,它是ThreadLocal
的一个静态内部类,是一个定制的散列映射(为什么说是定制呢,因为他没有继承或者实现任何Map接口或者其子类),只适用于维护线程本地值,即维护线程独享的本地变量。Thread 类中, threadLocals 默认为 null
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
可以看到 ThreadLocal.ThreadLocalMap确实没有继承或实现任何接口或类。
-
我们从 ThreadLocal 的get方法来分析。get方法的代码如下,看起来很少,其实真的很少。
public T get() { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程的 ThreadLocalMap ThreadLocalMap map = getMap(t); // 如果map不为空, 则从线程的ThreadLocalMap 中获取变量 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } .... // 获取线程中的 ThreadLocalMap ThreadLocalMap getMap(Thread t) { return t.threadLocals; } ... // 给 ThreadLocalMap 设置初始值 private T setInitialValue() { // 返回我们一开始给ThreadLocal设置的初始值 T value = initialValue(); // 获取当前线程 Thread t = Thread.currentThread(); // 获取线程中的 ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else // 为空则创建一个 ThreadLocalMap。并将value添加到map中,key是当前线程,value是初始值。 createMap(t, value); return value; } ... void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
代码注释比较详细,不做过多介绍。
从上面可以看到,当我们调用get
方法时,ThreadLocal
首先判断当前线程ThreadLocalMap
是否为空,不为空则从ThreadLocalMap
获取(由于ThreadLocalMap
是线程私有的,所以各个线程使用各自的ThreadLocalMap
并不会产生冲突)。若为空,则调用setInitialValued
方法为ThreadLocalMap
设置初始值。
2. ThreadLocal#set(T value)
public void set(T value) {
Thread t = Thread.currentThread();
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);
}
ThreadLocal#set(T value) 方法更加简单。和上面的 setInitialValue
方法基本相同,也不做过多介绍。
即,当调用set方法时,会先判断当前线程的ThreadLocalMap 是否为空,不为空,则直接放入,为空则创建一个map,再将值放入map中。
3. 总结
假设有线程A,B。有ThreadLocal L,初始值 i = 100;
- 线程A获取L中的值,调用L.get 方法,A.ThreadLocalMap (以下缩写成 A.M。B线程同理) 默认为null,所以会获取L 中的初始值 i,并在get方法中会创建A线程的ThreadLocalMap,并且将初始值i 添加进去,此时 A.M的值为[ThreadA.class, 100]。
- 当线程A调用L.set方法更新值为 999 时,由于A.M不为空,则会直接更新A.M中的值为999 。此时此时 A.M的值为[ThreadA.class, 999]。
- 当线程B调用L.get 方法时,.B.M默认为null, 所以会获取L 中的初始值 i,并在get方法中会创建B线程的ThreadLocalMap,并且将初始值i 添加进去,此时 B.M的值为[ThreadB.class, 100], A.M的值为[ThreadA.class, 999]。
四、ThreadLocal 使用中的问题
1. ThreadLocal 内存泄漏
首先需要了解Java中对象的四种引用方式,这里不是本文重点,所以就简单说一下。
- 强引用: 我们正常使用对象一般都是强引用。只要引用还存在,就不会回收内存,内存不足时会抛出OOM
- 软引用(SoftReference):当没有强引用存在时,内存足够时不会回收,内存不足时会回收
- 弱引用(WeakReference):当没有强引用存在时,不管内存是否足够,下一次GC时就会回收
- 虚引用(PhantomReference):和没有任何引用相同,在任何时候都可能被垃圾回收期回收,它不能单独使用也不能通过它访问对象。虚引用必须要和引用队列(ReferenceQueue)一起使用。
而在 ThreadLocal.ThreadLocalMap
中,内部类 Entry
继承了 WeakReference
弱引用。而引用的类型是ThreadLocal。这就表明ThreadLocal.ThreadLocalMap.Entry
中的key 是弱引用的,而value是强引用。所以当key被回收时,value还有强引用存在,则value无法回收,会造成内存泄漏。在最新的ThreadLocal中已经做出了修改,即在调用set、get、remove方法时,会清除key为null的Entry,但是如果不调用这些方法,仍然还是会出现内存泄漏 :),所以要养成用完ThreadLocal对象之后及时remove的习惯。
2. ThreadLocal 创建时一定不要传递对象的引用
这部分内容是在某一天某一时某一件事引起的某一个想法从而进行某一项验证发现的这一项总结。
话说那一天,那是鞭炮齐鸣,锣鼓喧闹,那家伙是人山人海。。。
事出有因,话不多说。直接看代码
由于包装类型会自动拆箱装箱,所以这里定义了一个Target类,里面只有一个name属性
public class Target {
private String name;
public Target(String name) {
this.name = name;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
1. Demo1
看下面代码, 代码很简单,就启动了两个线程,猜猜输出什么?
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
ThreadLocal<Target> threadLocal = ThreadLocal.withInitial(() -> new Target("张三"));
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get());
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get().getName());
Thread t1 = new Thread(() -> {
threadLocal.get().setName("李四");
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get());
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get().getName());
}, "t1");
Thread t2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get());
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get().getName());
}, "t2");
t1.start();
Thread.sleep(1000); // 睡眠1s是为了让t1执行完再执行t2
t2.start();
}
输入结果如下 :
上面这个结果很简单。可以看到 main线程、t1线程、t2线程所保存的 Target对象并不是同一个(至于为什么不是同一个,下面会讲解)。这时候可以正常使用,在t1线程中修改Target并不影响 t2线程。这符合我们对ThreadLocal 的理解。
2. Demo2
那么,下面就是见证奇迹的时刻 。如果我把代码改成下面的样子,就是在ThreadLocal 初始化时值的时候由 匿名类的方式 变成了传递 引用,那么这时候的执行结果是什么?。
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
Target target = new Target("张三");
ThreadLocal<Target> threadLocal = ThreadLocal.withInitial(() -> target); // 赋值方式改变
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get());
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get().getName());
Thread t1 = new Thread(() -> {
threadLocal.get().setName("李四");
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get());
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get().getName());
}, "t1");
Thread t2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get());
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get().getName());
}, "t2");
t1.start();
Thread.sleep(1000);
t2.start();
}
输出结果如下:
有没有发现出乎我们的意料,这里的三个线程 main、t1、t2保存的都是同一个对象,并且我在t1线程中的修改影响到了t2线程的内容。
是不是吓一跳(反正我是吓一跳),这样还怎么使用ThreadLocal。
3. 原因探究
- 首先需要明白ThreadLocal 实现的原理。总结起来就是: “你有就用你的,你没有就用我的”。
- 我们就用Demo1的代码做分析,让代码执行到t1线程中调用
threadLocal.get()
。这时候追溯源码进入get方法。根据上面的分析, t1线程进来时ThreadLocalMap
为null,所以会调用setInitialValue
方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
- 而在 setInitialValue 方法中的第一句, 我们看到变量就是从这里拿来的,那这一句就是问题的关键了。我们点进去看,发现
ThreadLocal
类并没有具体实现,仅仅是返回null。正所谓常言道:事有反常必为妖。
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
...
protected T initialValue() {
return null;
}
- 然后我们就发现在ThreadLocal 类中有一个ThreadLocal 的子类
SuppliedThreadLocal
。而我们回头初始化ThreadLocal 值的时候可以发现,我们获取的就是这个子类,那么我们就可以确定。
. - 再看 SuppliedThreadLocal 我们发现,initialValue的值 从 Supplier.get()方法来,而我们正好在初始化ThreadLocal 时实现了这个接口。
ThreadLocal<Target> threadLocal = ThreadLocal.withInitial(() -> new Target("张三")); // 等同于。 ThreadLocal<Target> threadLocal = ThreadLocal.withInitial(new Supplier<Target>() { @Override public Target get() { return new Target("张三"); } });
- 总结如下: 当t1线程调用
ThreadLocal
时因为ThreadLocalMap
为null
,所以会调用setInitialValue
方法获取初始值,setInitialValue
又会调用initialValue
方法初始化一个初始值,而我们初始化初始值时又重写了initialValue
方法。也就是说,当Demo1中 t1 调用ThreadLocal.get
时,也就是又调用了一次initialValue
方法,也就重新new Target("张三")
。所以三个线程执行ThreadLocal.get
执行出来的结果并不一样,因为他们每次拿到的对象并不一样。而Demo2中,因为我们传递的是一个target
引用。导致三个线程获取的都是这一个对象,并且由于ThreadLocal
中并没有深拷贝,所以他们实际操作的是同一个对象,从而造成的Demo2的结果。
- 总结如下: 当t1线程调用
以上:内容部分参考
https://www.jianshu.com/p/1ff73d2d7520
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正