持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情
摘要
在多线程环境下,最常用的实现线程安全的方式有如下几种:
- 互斥同步机制,例如 Lock,synchronized 关键字,信号量等。
- 非阻塞同步机制,例如自旋 + CAS。
- 线程独享机制,即不在多线程间共享,例如本文要学习的 ThreadLocal。
那么,ThreadLocal 是如何实现每个线程独有一份副本的?
01-ThreadLocal 线程隔离原理
当我们在某个类中使用 ThreadLocal 类型的变量时,是如何做到每个线程存储一份副本的呢?我们知道,在 Java 中,每个线程都是一个独立的 Thread 对象。要实现线程之间互相独立,肯定要在 Thread 对象上做功夫。
Thread 类中有如下的一个属性:
ThreadLocal.ThreadLocalMap threadLocals = null;
当我们使用 ThreadLocal 对象获得值时,一般会用如下的写法:
ThreadLocal<Object> tl = new ThreadLocal();
Object obj = tl.get();
让我们看一下,当我们调用 get 方法时,做了什么操作:
public T get() {
/** 获得调用此方法的当前线程 */
Thread t = Thread.currentThread();
/** getMap 返回的就是 Thread 中的 threadLocals 对象 */
ThreadLocalMap map = getMap(t);
if (map != null) {
/** 此处的 this 指的是上面的 tl 对象*/
ThreadLocalMap.Entry e = map.getEntry(this);
/** 检查 threadLocals 中是否存在与 tl 相关的值(以 tl 为键的值) */
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
/** 如果 t.threadLocals 为空,则为其初始化一个,
* 并将 initialValue() 方法的返回值写入 */
return setInitialValue();
}
02-示例分析
知道 ThreadLocal 实现线程间隔离的原理后,我们来分析一个实例,来具体看一下整个过程。假设我们有如下的程序:
/** private static 是一个典型的写法 */
private static ThreadLocal<String> dbConnection = new ThreadLocal<String>() {
@Override
protected String initialValue() {
Thread t = Thread.currentThread();
return t.getName();
}
};
public String getConnection() {
return dbConnection.get();
}
假设有线程 t1 / t2:
new Thread(() -> {
String str = getConnection();
System.out.println(Thread.currentThread().getName() + " str = " + str);
}, "thread-t1").start();
new Thread(() -> {
String str = getConnection();
System.out.println(Thread.currentThread().getName() + " str = " + str);
}, "thread-t2").start();
当线程 t1 执行到 getConnection 时,会调用 ThreadLocal 中的 get 方法。在 get 方法中,先获取当前线程,即 t1。然后,获取 t1 的 threadLocals 映射表。此时,t1.threadLocals 为 null,即尚未初始化。然后,调用 setInitialValue,再到我们重写的方法 initialValue,返回当前线程 t1 的线程名”thread-t1“。最后,初始化 t1.threadLocals 并将 dbConnection 对象和”thread-t1”写入到 t1.threadLocals 中。
线程 t2 的执行过程与 t1 一样。因为 t1.threadLocals 和 t2.threadLocals 是两个 Thread 对象中的变量,所以互相不干扰。这样,借助 ThreadLocal 线程 t1 和 t2 实现了 dbConnection 的隔离访问。
03-ThreadLocal 导致的内存泄漏问题
ThreadLocal 有可能会导致内存泄漏的发生。从前面的学习中我们了解到,Thread 类中存在一个 ThreadLocal.ThreadLocalMap 对象,它是一个映射表,key 是对 ThreadLocal 对象(若引用),value 是一个特定类型的值。
内存泄漏出现的原因是,假如某个 ThreadLocal 对象的强引用不在了,其势必会被 GC 回收掉。但是,Thread 类中的 ThreadLocal.ThreadLocalMap 对象的生存周期与线程对象是一致的。当 ThreadLocal 对象被回收后,就无法访问其对应的 value,导致内存一致不能释放。
有一种常见的错误就是,在线程池中不恰当地使用 ThreadLocal,线程池中的线程对象一直存在,当上述情况发生时,发生内存泄漏的机率更大。
ThreadLocal 推荐的使用方法是:
- 每次使用完 ThreadLocal 后,就调用它的 remove 方法,移除 value
- 将 ThreadLocal 对象声明成 private static,确保存在强引用而不会被 GC 回收,从而能够访问到其对应的 value。
[1] 面试:为了进阿里,死磕了ThreadLocal内存泄露原因
历史文章推荐
- Java Core 「16」J.U.C Executor 框架之 ScheduledThreadPoolExecutor
- Java Core 「15」J.U.C Executor 框架
- Java Core 「14」J.U.C 线程池-Future & FutureTask
- Java Core 「13」ReentrantReadWriteLock 再探析
- Java Core 「12」ReentrantLock 再探析
- Java Core 「11」AQS-AbstractQueuedSynchronizer
- Java Core 「10」J.U.C 同步工具类-2
- Java Core 「9」J.U.C 同步工具类-1