ThreadLocal无论是在项目开发还是面试中都会经常碰到,它的重要性可见一斑,本篇文章就从ThreadLocal的使用、实现原理、核心方法的源码、内存泄漏问题等展开介绍一下。
ThreadLocal是java.lang下面的一个类,在JDK 1.2版本加入,作者是Josh Bloch(集合大神)和Doug Lea(并发大神)。
它提供了一种线程局部变量的方式,线程局部变量是指每个线程都拥有自己独立的变量副本,互不干扰,通过ThreadLocal,可以方便地在多线程环境下共享数据,同时不需要考虑线程安全性,这也是解决并发问题的途径之一。
例如:在web开发中,可以使用ThreadLocal来保存用户的登录信息,以便每个线程都能够独立地获取和修改自己的登录信息,避免了线程之间的干扰。
ThreadLocal有四个方法,分别为:
protected T initialValue():返回此线程局部变量的初始值。
pubulic T get(): 返回当前线程局部变量的当前线程副本的值。如果这是线程第一次调用该方法,则创建并初始化此副本。
public void set(T value):将此线程局部变量的当前线程的副本设置为指定的值。
public void remove():移除此线程局部变量的当前线程的值。
下面使用ThreadLocal来模拟用户登录信息的场景:
ThreadLocal工具类:
public class CurrentUserHolder {
public static ThreadLocal threadLocal=new ThreadLocal<>();
public static void setUser(User user){
threadLocal.set(user);
}
public static User getUser(){
if (Objects.nonNull(threadLocal.get())) {
return threadLocal.get();
}
throw new RuntimeException("当前用户信息为空!");
}
public static void clearUser(){
threadLocal.remove();
}
}
User实体类:
@Data
public class User {
private String name;
private Integer age;
}
测试:
public class Test {
public static void main(String[] args) {
//用户登录
User user = new User();
user.setName("小黑子");
user.setAge(18);
//将用户信息保存在ThreadLocal中
CurrentUserHolder.setUser(user);
//在其它方法中,可以通过ThreadLocal获取用户信息
User localUser = CurrentUserHolder.getUser();
System.out.println(localUser);//输出:User(name=小黑子, age=18)
//用户操作完成后,可以remove掉
CurrentUserHolder.clearUser();
}
}
ps:由于ThreadLocal是基于线程的,所以在不同的线程中,通过ThreadLocal获取的用户信息是独立的,这在多线程环境下非常有用,可以避免线程之间的数据混乱和冲突。
直接上图!下图中基本描述出了Thread、ThreadLocalMap和ThreadLocal三者之间的关系。
解释一下:
ThreadLocal中用于保存线程的独有变量的数据结构是一个内部类:ThreadLocalMap,也是k-v结构,key就是当前ThreadLocal对象,value就是我们要保存的值。
Thread类中维护了两个ThreadLocalMap成员变量,threadLocals和inheritableThreadLocals,它们的默认值是null,类型为ThreadLocal.ThreadLocalMap,也就是ThreadLocal类的一个静态内部类ThreadLocalMap,感兴趣的可以去看一下源码。
在静态内部类ThreadLocalMap中,维护了一个数据结构类型为Entry的数组,源码如下:
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
从源码中我们可以看到,Entry继承了一个ThreadLocal类型的弱引用并将其作为key,value为Object类型(也就是我们需要保存的值)。
我们再来看一下它的成员变量:
//数组的默认初始化容量
private static final int INITIAL_CAPACITY = 16;
//Entry数组,大小必须为2的幂
private Entry[] table;
//数组内部元素个数
private int size = 0;
//数组扩容阈值,默认为0,创建ThreadLocalMap对象后会被重新设置
private int threshold;
是不是有点熟悉,这几个变量和HashMap中的变量很类似,功能也类似。
最后看一下它的构造方法:
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
注释翻译过来大概就是,该构造方法是懒加载的,只有我们创建一个Entry对象并需要放入到Entry数组的时候才会去初始化数组。
接下来我们就介绍一下ThreadLocal常用的一些方法吧,首先看一下set()方法:
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null)
// 如果map存在,则将当前ThreadLocal对象作为key,value作为value放入map中
map.set(this, value);
else
// 如果map不存在,则创建一个新的ThreadLocalMap对象,并新建一个Entry放入该ThreadLocalMap, 调用set方法的ThreadLocal和传入的value作为该Entry的key和value
createMap(t, value);
}
解释:
获取当前线程,拿到当前Thread的ThreadLocalMap对象。
如果map存在,则将当前ThreadLocal对象作为key,value作为value放入map中。
如果map不存在,则创建一个新的ThreadLocalMap对象,并新建一个Entry放入该ThreadLocalMap, 调用set方法的ThreadLocal和传入的value作为该Entry的key和value。
源码如下:
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//map存在,通过this(当前ThreadLocal)获取Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//Entry不为空,返回该Entry的value值
T result = (T)e.value;
return result;
}
}
//map不存在,调用setInitialValue()方法设置初始值
return setInitialValue();
}
解释:
通过当前线程获取ThreadLocalMap:
如果map存在,则通过当前ThreadLocal获取对应的Entry,若Entry不为空,返回该Entry的value值。
如果map不存在,则调用setInitialValue()方法设置初始值。
setInitialValue():
根据initalValue()方法获取value值,默认值为null,可以重写该方法。
通过当前线程获取ThreadLocalMap对象。
map存在,设置当前值为上述value,不存在则创建新的ThreadLocalMap,并将值设置为value。
源码如下:
public void remove() {
//根据当前线程获取ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//存在,执行remove方法
m.remove(this);
}
解释:
根据当前线程获取ThreadLocalMap对象,存在则执行remove()方法。remove(this)方法中,将ThreadLocal作为key来删除对应的Entry。
读到这,相信你对ThreadLocal的基本原理有了更深一步的理解,我们把上图补全,从堆栈视角看一下它们之间的引用关系。
我们可以看到,ThreadLocal对象,有两个引用,一个是栈上的ThreadLocal引用,一个是ThreadLocalMap中Key对它的引用。如果栈上的ThreadLocal引用不再使用了,那么ThreadLocal对象因为还有一条引用链在,所以会导致它无法回收,久而久之就会OOM。
这就是我们所说的ThreadLocal的内存泄漏问题,为了解决这个问题,ThreadLocalMap使用了弱引用,就是上述我们说过的Entry数组:
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
可以看出,ThreadLocal的引用k通过构造方法传递给了Entry类的父类WeakReference的构造方法,那么可以理解为ThreadLocalMap中的键是ThreadLocal的弱引用。
穿插一下Java中的四大引用:
强引用:Java中默认的引用类型,只要引用还存在,即便OOM也不会被回收。
软引用:内存不足时,将会被干掉。
弱引用:无论内存充足与否,只要执行GC,就会被干掉。
虚引用:最弱的一种引用,存在意义就是为了将关联虚引用的对象在被GC掉之后收到一个通知。
如果用了弱引用,那么ThreadLocal对象就可以在下次GC的时候被回收掉了。
这样做可以很大程度上避免了因为ThreadLocal的使用而导致的OOM问题,但也无法彻底避免。
我们可以看到,虽然key是弱引用,但是value是强引用,而且它的生命周期是和Thread一样的,也就是说,只要Thread还在,那么这个对象就无法被回收。
那么,什么情况下,Thread会一直在呢,那就是线程池,这就导致value一直无法被回收。
ThreadLocalMap底层使用数组来保存元素,使用“线性探测法”来解决hash冲突,在每次调用ThreadLocal的get、set、remove方法时,内部会实际调用ThreadLocalMap的get、set、remove等操作,而ThreaLocalMap的每次set、get、remove时,都会对key为null的Entry进行清除(expungeStateEntry()方法,将Entry的value清空,等下次GC就会被回收)。
所以,当我们一个ThreadLocal用完后,就手动remove一下,就可以在下次GC时,把Entry清理掉。
上述我们分了两种情况来看ThreadLocal内存泄漏问题:
key使用强引用:引用ThreadLocal的对象被回收了,但是ThreadLocalMap持有ThreadLocal的强引用,如果没有手动remove,ThreadLocal不会被回收,导致Entry内存泄漏。
key使用弱引用:引用ThreadLocal被回收,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动remove,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动remove,就会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal被清理后key为null,对应的value在下一次ThreadLocalMap调用set、get、remove的时候可能会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期和Thread一样长,如果没有手动remove就会导致内存泄漏,而不是因为弱引用。
End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教。