文章目录
引入
ThreadLocal<Object> tl = new ThreadLocal<>();
tl.set(new Object());
在工作中,我们通常会这么使用ThreadLocal,那么这个ThreadLocal到底是什么呢?为什么会能够起到线程隔离的作用?内存泄漏又是什么呢
基础概念
ThreadLocal:线程本地变量,通过set和get方法来存储和获取当前线程封装的唯一的数据结构
java四大引用类型:
-
强引用
使用=
来表示强引用,例如Object obj = new Object()
, 这里的obj就是一个强引用,指向堆空间中开辟的一片Object区域
特点:jvm宁愿抛OOM也不会回收强引用指向的对象 -
弱引用
使用WeakReference
关键字定义的引用
特点:如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉 -
软引用
使用SoftReference
关键字定义的引用
特点:只有堆内存空间不够用的时候才会被回收 -
虚引用
使用PhantomReference
关键字定义的引用
特点:跟踪对象被垃圾回收的状态,仅仅是提供了一种确保对象被finalize以后,做某些事情的机制
ThreadLocal、ThreadLocalMap、Entry、Thread之间的关系
了解完以上内容后,我们来梳理一下三者的一个关系:
如下图,ThreadLocalMap是定义在ThreadLocal 的内部类,在ThreadLocalMap中又定义了Entry,这个Entry的键的类型为ThreadLocal类型,并且是一个弱引用
ThreadLocal源码
我们结合上图看ThreadLocal类的set方法的源码
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
主要包含这几个步骤
1.获取当前执行中的线程
2.获取线程中的ThreadLocalMap对象
3.第一次获取会new一个新的ThreadLocalMap,并set值
4.如果创建过了,就直接往map中set值
get方法
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方法
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;
}
我们都知道在Java中成员变量都会有默认值,而ThreadLocal调用get时也会有默认值,在必要的情况下,我们可以通过重写initialValue方法设定ThreadLocal.get返回变量的初始值。默认情况下initialValue返回的是null。
get方法主要包含这几个步骤
1.获取当前执行中的线程
2.获取线程中的ThreadLocalMap对象
3.如果之前这个线程set过值,就会取出对应的value
4.如果之前这个线程没有set过值,就先set一个null,再返回null
remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
将当前线程局部变量的值删除,目的是为了减少内存的占用。该方法是JDK5.0 新增的方法。
当线程结束后,对应该线程的局部变量将自动被垃圾回收,因此调用该方法清除线程的局部变量并不是必须的操作,但调用它可以使内存回收更加及时,加速内存的回收。
为什么使用弱引用
我们知道,ThreadLocalMap中的Entry结构的Key用到了弱引用
而当ThreadLocalRef被回收时,强引用消失,此时ThreadLocal只有一个弱引用指向,因此Heap中的ThreadLocal会被回收,这就是为什么key为什么使用弱引用的原因
为什么ThreadLocal能够起到线程隔离的作用呢
其实ThreadLocal本身不存放任何的数据,而ThreadLocal中的数据实际上是存放在线程实例中,ThreadLocal存入值时使用当前ThreadLocal实例作为key,存入当前线程对象中的Map中去,因此不同线程调用ThreadLocal.get时,都是从各自的ThreadLocalMap中获取。
内存泄漏的原因
当ThreadLocal被回收后,ThreadLocalMap中对应的Key就会指向null,而对应Value却不为null,这些value项如果不主动清理,就会一直驻留在ThreadLocalMap中。
如何避免内存泄漏
- 每次使用完ThreadLocal后都使用 try-finally 块调用remove方法确保清空value
- ThreadLocal变量设置为static final 共用一个,保证强引用
- ThreadLocal内部的优化,会在我们调用set方法后,进行全量清理,清理出key为null的值,扩容也会继续检查
- 调用get方法后,没有直接命中或向后环形清除,会进行清理
- 调用remove方法的时候,会向后进行环形清理
3、4 、5三种方式是ThreadLocal内部为我们做一些措施,但是这些措施并不能完全帮助我们完全避免保证内存泄漏,这是因为
- 使用线程池的时候,这个线程执行任务结束,
ThreadLocal
对象被回收了,线程放回线程池中不销毁,这个线程一直不被使用,导致内存泄漏。 - 分配使用了
ThreadLocal
又不再调用get()
,set()
,remove()
方法,那么这个期间就会发生内存泄漏。
因此这里我们建议大家使用1+2结合的方式