再谈 ThreadLocal
几年前我曾经写过两篇关于 ThreadLocal 的文章,分别是ThreadLocal类之简单理解和ThreadLocal类之简单应用示例,不过限于当时的水平,有些问题并没有说的很明白,所以今天再写一篇文章,重新说说这个类。
我们首先看一个例子:
package cn.bridgeli.demo;
/**
* @author BridgeLi
* @date 2021/4/21 11:02
*/
public class User {
String name = "Denny";
}
然后我们有一个操作:
package cn.bridgeli.demo;
import org.junit.Test;
/**
* @author BridgeLi
* @date 2021/4/21 10:28
*/
public class ThreadTest {
private User user = new User();
@Test
public void testThreadLocal() {
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(user.name);
}).start();
new Thread(() -> user.name = "BridgeLi").start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这个时候我们就知道一定会有线程安全问题,所以我们怎么解决这个问题呢?就是 ThreadLocal,请看下面:
package cn.bridgeli.demo.reference;
import org.junit.Test;
/**
* @author BridgeLi
* @date 2021/4/21 10:28
*/
public class ThreadLocalTest {
private static ThreadLocal<User> threadLocal = new ThreadLocal<>();
@Test
public void testThreadLocal() {
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadLocal.get());
}).start();
new Thread(() -> threadLocal.set(new User())).start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这个时候我们发现,一个线程里面放入的对象,我们在另一个线程拿不到,就这样解决了线程安全问题,那么这个又是如何做到的呢?我们通过源码分析,我们首先看 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);
}
第一步先获取 当前线程,第二步获取当前线程的一个属性:threadLocals,类型是 ThreadLocalMap,这一步也就是说,在不同的线程里面,获取到的 t 对象肯定不是同一个,那么我们的 map 对象当然也就不是同一个了,所以下一步 set 的时候,不同的线程,也就 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();
}
前面两步一样,都是获取当前线程,然后取当前线程上的 ThreadLocalMap,然后从 ThreadLocalMap 中获取存储的值,所以我们就轻而易举的理解了,上面的例子,为什么一个线程存,另一个线程取不到。那么这篇文章就到此结束了吗?当然没有。下面我们接着说 set 方法,我们先说第一个问题:
1. set 的时候的 key
我们很明显看到是 this,那么 this 是什么?就是 ThreadLocal 对象,到我们上面那个具体的例子就是我们定义的那个属性:threadLocal,那么这个时候很多人就有了一个问题:一个 ThreadLocal 只能存一个对象吗?答案肯定是:是的,因为你在存第二个对象的时候,由于 this 是同一个,所以一定会被覆盖掉,所以一个 ThreadLocal 对象只能存储一个对象,那么很多人很快就有了第二个疑问:那么我有两个对象要存怎么办?答案也很简单,再定义一个属性:threadLocal2 喽,不然还能咋办,那么此时两个对象是怎么存储的呢?我们可以想象的到,他们都在当前线程的一个 ThreadLocalMap 中存储,但是他们的 key 是不同的,分别对应不同的 ThreadLocal 对象,所以相安无事。下面我们接着研究第二个问题:
2. ThreadLocal 的内存泄漏问题
之前看到过一些文章,ThreadLocal 有内存泄漏问题,也有人说没有,那么 ThreadLocal 是否真的有内存泄漏问题?如果没有,为什么?如果有,同样为什么?看 ThreadLocal 有没有内存泄漏问题,这个时候我们就需要研究 ThreadLocalMap 这个对象了,所以我们接着看一个这个对象有何特点。
我们通过源码可以看到,我们初始化线程的时候有一行代码:
ThreadLocal.ThreadLocalMap threadLocals = null;
也就是说 ThreadLocalMap 的初始值是 null,所以我们 set 的时候,getMap(t) 得到的一定是 null,代码进入到 else 取执行:createMap(t, value),这个方法很简单就一行实现:
t.threadLocals = new ThreadLocalMap(this, firstValue);
也就是 new 了一个 ThreadLocalMap 对象,赋值给了当前线程的 threadLocals 属性,所以我们要看 ThreadLocalMap 的构造方法:
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);
}
也很简单 new 了一个 Entry 对象,这个我们都很熟了,和 HashMap 很类似,那么他和 HashMap 的 Entry 是不是一样的呢?我们接着看一下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
我们可以看到这个 Entry 有点不太一样,他继承了 WeakReference 对象,而且他的构造方法第一行调用了 super(k),所以这个时候就很明显了,这个 k 就是我们定义的 ThreadLocal 属性,而这个时候,又调用了 WeakReference 的构造方法,我之前曾写过一篇文章:Java 的引用类型和使用场景,里面讲到了弱引用,也就是 WeakReference,他有什么特点?遇到 GC,不管三七二十一,立马就被回收了,所以就是基于此,有人说:ThreadLocal 没有了内存泄漏问题,因为 GC 之后,ThreadLocal 对象就会被回收,这个说法看似很正确,但是其实真的是这样的吗?我们再仔细看一下 Entry 的构造方法,他的 key 是 WeakReference,但是他的 value 呢?那可是一个扎扎实实的强引用,所以 GC 之后 key 被回收了,没有问题,value 咋办?还在占用内存,只是访问不到了而已?这块内存一直不会被释放,那么这不是内存泄漏是什么?所以 ThreadLocal 如果使用不当依然会有内存泄漏问题,使用的时候还是要小心滴。
看完上面这段话,有人发现,哎,不对啊,你不说我还用的好好的,你一说,把我说糊涂了,糊涂在哪呢?就在 WeakReference 这,WeakReference 是弱引用没错的,k 也确实是 ThreadLocal 对象,按照弱引用的特点,一 GC 不管三七二十一就把这个 key 回收了,在我们的系统中 GC 是随时都有可能发生的,而且不是受我们控制的,那么我们怎么取出的我们放进去的对象的呢?GC 之后不应该取不到了吗?其实这个问题,还是有些人没想清楚,我们再回过头来看一下我们最开始定义 ThreadLocal 的时候的代码:
private static ThreadLocal<User> threadLocal = new ThreadLocal<>();
这是一个啥?一个标标准准的强引用,所以我们不用担心 GC 之后,ThreadLocal 对象被回收,取不到我们存入的对象的问题,但是如果我们在使用的过程中把他赋值为 null,那么下次 GC 的时候,一定会被回收掉的,这是毫无疑问的。所以这个时候,又有同学有疑问了,那么既然如此:k 为什么还要定义成 WeakReference 类型的呢?这就是一个说法的问题,如果不定义成 WeakReference 类型的,那么一个内存就会有两个强引用存在,一个是我们定义的 threadLocal,另一个是这个 k,所以当我们把 threadLocal 赋值成 null 之后,我们会发现,无论如何我们也回收不掉这块内存,那么这不就是说 ThreadLocal 对象存在内存泄漏问题吗?
所以综上,我们可以得出这样一个结论:由于 WeakReference 的存在,ThreadLocal 对象本身没有内存泄漏问题,但是如果使用不当,我们存入的 value 会有内存泄漏问题。
看到这,又有同学有疑问了,那么 value 为啥,不也定义成 WeakReference 类型呢?那么不就 k 和 value 都没有内存泄漏问题了?其实原因也很简单,我们的 k 定义成 WeakReference 类型,不怕被回收,是因为有我们自己定义的 threadLocal 属性,这个强引用在,那么 value 呢?他没有啊,所以如果 value 也是 WeakReference 类型,那么一遇到 GC 完蛋了,我们放入的对象没了,所以 value 不能定义成 WeakReference 类型,那么既然如此,我们又该如何避免 value 的内存泄漏问题呢?答案很简单,使用完 ThreadLocal 之后,记得调用一下 remove 方法即可,具体就不再多说了。最后上一张 ThreadLocal 的内存结构图:

作 者: BridgeLi,https://www.bridgeli.cn
原文链接:http://www.bridgeli.cn/archives/706
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。
近期评论