如果有兴趣了解更多相关内容,欢迎来我的个人网站看看:瞳孔空间
ThreadLocal类能提供线程内部的局部变量。这种变量在多线程环境下访问时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。
归纳要点,即:
ThreadLocal有以下四个常用方法:
方法声明 | 描述 |
---|---|
ThreadLocal() | 创建ThreadLocal对象 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove | 移除当前线程绑定的局部变量 |
现在有如下使用场景:
代码如下:
/**
* @author eyes
* @date 2023/1/21 9:15
*/
@Data
public class Demo {
// 变量
private String content;
public static void main(String[] args) {
Demo demo = new Demo();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
demo.setContent(Thread.currentThread().getName());
System.out.println(""); // 打印这个是为了让线程在setContent之后不立即执行下面的输出,让访问错乱的效果更明显
System.out.println(Thread.currentThread().getName() + "----->" + demo.getContent());
}, "线程" + i).start();
}
}
}
可见如果不将线程隔离,那么多线程并发场景下就会导致错乱,为此可以使用ThreadLocal进行改进:
/**
* @author eyes
* @date 2023/1/21 9:15
*/
public class Demo {
ThreadLocal<String> tl = new ThreadLocal<>();
private String getContent() {
return tl.get();
}
private void setContent(String content) {
tl.set(content);
}
public static void main(String[] args) {
Demo demo = new Demo();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
demo.setContent(Thread.currentThread().getName());
System.out.println(""); // 打印这个是为了让线程在setContent之后不立即执行下面的输出,让访问错乱的效果更明显
System.out.println(Thread.currentThread().getName() + "----->" + demo.getContent());
}, "线程" + i).start();
}
}
}
如果我们不去看源代码的话,可能会猜测ThreadLocal是这样子设计的:每个ThreadLocal都创建一个Map,然后用线程作为Map的key,要存储的局部变量作为value,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK最早期的ThreadLocal确实是这样设计的,如下图:
但是,JDK后面优化了设计方案,在JDK8中 ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value才是真正要存储的值object。具体的过程是这样的:
两者对比可知,JDK8的设计方案有如下好处:
基于ThreadLocal的内部结构,我们继续分析它的核心方法源码,更深入的了解其操作原理。ThreadLocalMap是ThreadLocal的静态内部类,由于内容较多,因此ThreadLocalMap单独放到3.3中介绍。
除了构造方法外,ThreadLocal对外暴露的方法有以下4个:
方法声明 | 描述 |
---|---|
protected initialValue() | 返回当前线程局部变量的初始值 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove | 移除当前线程绑定的局部变量 |
/**
* 设置当前线程对应的ThreadLocal的值
*
* @param 将要保存在当前线程对应的ThreadLocal的值
*/
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 判断map是否存在
if (map != null) {
// 存在则调用map.set设置此实体entry
map.set(this, value);
} else {
// 当前线程Thread不存在ThreadLocalMap对象则创建该对象
// 并将t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
}
}
/**
* 获取当前线程Thread对应维护的ThreadLocalMap
*
* @param t 当前线程
* @return 对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* 创建当前线程Thread对应维护的ThreadLocalMap
*
* @param t 当前线程
* @param firstValue 存放到map中的第一个entry的值
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* 返回当前线程中保存ThreadLocal的值
* 如果当前线程没有此ThreadLocal变量,则调用initialvalue方法进行初始化值
*
* @return 当前线程对应此ThreadLocal的值
*/
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以当前的ThreadLocal为key,调用getEntry获取对应的存储实体e
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 获取存储实体e对应的value值
// 即为我们想要的当前线程对应此ThreadLocal的值
T result = (T)e.value;
return result;
}
}
// 调用initialvalue方法进行初始化值
// 有两种情况会执行当前代码
// 1. map不存在,表示此线程没有维护的ThreadLocalMap对象
// 2. map存在,但是没有与当前ThreadLocal关联的entry
return setInitialValue();
}
/**
* set()方法的变种,用以构建初始化值
* 当set()方法被重写时用以替代原set()方法
*
* @return 初始化值
*/
private T setInitialValue() {
// 调用initialValue获取初始化的值
// 此方法可以被子类重写,如果不重写则默认返回null
T value = initialValue();
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// map存在则调用map.set()设置此实体类
map.set(this, value);
} else {
// map不存在则调用createMap进行ThreadLocalMap对象的初始化
// 并将t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
/**
* 删除当前线程中保存的ThreadLocal对应的实体entry
*/
public void remove() {
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
// map存在则删除对应entry
m.remove(this);
}
}
/**
* 返回当前线程对应的ThreadLocal的初始值
* 当线程没有先调用set方法就调用get方法时,此方法才会执行
*
* 这个方法仅仅简单返回null,如果程序员想ThreadLocal线程局部变量有一个
* 除null以外的初始值,必须通过子类继承的方式去重写此方法,通常可以用匿名内部类实现
* 该方法是protected,显然是为了让子类覆盖而设计的
*
* @return 当前ThreadLocal的初始值
*/
protected T initialValue() {
return null;
}
匿名内部类重写initialValue方法:
ThreadLocal tl = new ThreadLocal() {
@Override
protected String initialValue() {
return "瞳孔";
}
};
除此之外,ThreadLocal还提供了一个便捷的静态方法:
ThreadLocal<String> tl = ThreadLocal.withInitial(() -> "瞳孔");
ThreadLocalMap是ThreadLocal的静态内部类,虽然它叫map,但并没有实现Map接口,它用独立的方式实现了Map的功能,其内部的Entry也是独立实现的。
/**
* 初始容量 -- 必须是2的整次幂
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 存放数据的table, 大小会根据需要调整
* 数组长度必须是2的整次幂
*/
private Entry[] table;
/**
* 数组里entrys的个数,可以用于判断table当前使用量是否超过阈值
*/
private int size = 0;
/**
* 进行扩容的阈值,表使用量大于它的时候进行扩容
* 默认为0
*/
private int threshold;
ThreadLocalMap用Entry来保存K-V结构数据。不过Entry的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。
另外,Entry继承WeakReference,也就是key(Threadlocal)是弱引用,其目的是将Threadlocal对象的生命周期和线程生命周期解绑。
/**
* Entry继承WeakReference,并且用ThreadLocal作为key
* 如果key为null(entry.get() == null),意味着key不再被引用,此时entry可以从table中清除
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
不了解JVM的话建议先看看JVM垃圾回收再看,推荐这篇:JVM详解——垃圾回收
内存泄漏相关概念:
Java中的引用有4种类型:强引用、软引用、弱引用和虚引用。当前这个问题只涉及强引用和弱引用。
有时候使用ThreadLocal的过程中会发现有内存泄漏的情况发生,就猜测这个内存泄漏跟Entry中使用了弱引用的key有关系。这个理解其实是不对的。先看下面这张图,假设Entry用的是强引用:
假设在业务代码中使用完ThreadLocal,ThreadLocal Ref被回收了。但是因为ThreadLocalMap的Entry强引用了ThreadLocal,造成ThreadLocal无法被回收。在没有手动删除这个Entry以及CurrentThread依然运行的前提下,
始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。也就是说,ThreadLocalMap中的key使用了强引用,也是无法完全避免内存泄漏的。
下面是key使用弱引用的情况:
同样假设在业务代码中使用完ThreadLocal,ThreadLocal Ref被回收了。由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向Threadlocal实例,所以Threadlocal就可以顺利被gc回收,此时Entry中的key=null。但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 ThreadRef->currentThread->threadLocalMap->entry ->value,value不会被回收,而这块value永远不会被访问到了,导致value内存泄漏。也就是说,ThreadLocalMap中的key使用了弱引用,也有可能内存泄漏。
因此我们可以知道,无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。
要避免内存泄漏有两种方式:
相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。
那么为什么key要用弱引用呢?事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。这就意味着使用完ThreadLocal , CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set、get、remove中的任一方法的时候会被清除,从而避兔内存泄漏。
和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。ThreadLocalMap解决Hash冲突的方式就是简单的步长加1,寻找下一个相邻的位置。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
// 在这里调用nextIndex选择数据存入的位置
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。