ThreadLocal 的使用方法
static class SafeDateFormat {
// 定义 ThreadLocal 变量
static final ThreadLocal<DateFormat>tl = ThreadLocal.withInitial(()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
static DateFormat get(){
return tl.get();
}
}
// 不同线程执行下面代码
// 返回的 df 是不同的
DateFormat df = SafeDateFormat.get();
ThreadLocal 的工作原理
错误的实现方案
class MyThreadLocal<T> {
Map<Thread, T> locals = new ConcurrentHashMap<>();
// 获取线程变量
T get() {
return locals.get(Thread.currentThread());
}
// 设置线程变量
void set(T t) {
locals.put(Thread.currentThread(), t);
}
}
JDK实现方案
Thread 这个类内部有一个私有属性 threadLocals,其类型就是 ThreadLocalMap,ThreadLocalMap 的 Key 是ThreadLocal。
class Thread {
// 内部持有 ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals;
}
class ThreadLocal<T>{
public T get() {
// 首先获取线程持有的
//ThreadLocalMap
ThreadLocalMap map = Thread.currentThread().threadLocals;
// 在 ThreadLocalMap 中
// 查找变量
Entry e = map.getEntry(this);
return e.value;
}
static class ThreadLocalMap{
// 内部是数组而不是 Map
Entry[] table;
// 根据 ThreadLocal 查找 Entry
Entry getEntry(ThreadLocal key){
// 省略查找逻辑
}
//Entry 定义
static class Entry extends WeakReference<ThreadLocal>{
Object value;
}
}
}
ThreadLocal 仅仅是一个代理工具类,内部并不持有任何与线程相关的数据,所有和线程相关的数据都存储在 Thread 里面,这样的设计容易理解。而从数据的亲缘性上来讲,ThreadLocalMap 属于 Thread 也更加合理。
当然还有一个更加深层次的原因,那就是不容易产生内存泄露。在我们的设计方案中,ThreadLocal 持有的 Map 会持有 Thread 对象的引用,这就意味着,只要 ThreadLocal对象存在,那么 Map 中的 Thread 对象就永远不会被回收。ThreadLocal 的生命周期往往都比线程要长,所以这种设计方案很容易导致内存泄露。而 Java 的实现中 Thread 持有ThreadLocalMap,而且 ThreadLocalMap 里对 ThreadLocal 的引用还是弱引用(WeakReference),所以只要 Thread 象可以被回收,那么 ThreadLocalMap 就能被回收。Java 的这种实现方案虽然看上去复杂一些,但是更加安全。
ThreadLocal 与内存泄露
在线程池中使用 ThreadLocal 为什么可能导致内存泄露呢?原因就出在线程池中线程的存活时间太长,往往都是和程序同生共死的,这就意味着 Thread 持有的 ThreadLocalMap一直都不会被回收,再加上 ThreadLocalMap 中的 Entry 对 ThreadLocal 是弱引用(WeakReference),所以只要 ThreadLocal 结束了自己的生命周期是可以被回收掉的。但是 Entry 中的 Value 却是被 Entry 强引用的,所以即便 Value 的生命周期结束了,Value 也是无法被回收的,从而导致内存泄露。
那在线程池中,我们该如何正确使用 ThreadLocal 呢?其实很简单,既然 JVM 不能做到自动释放对 Value 的强引用,那我们手动释放就可以了。
ExecutorService es;
ThreadLocal tl;
es.execute(()->{
//ThreadLocal 增加变量
tl.set(obj);
try {
// 省略业务逻辑代码
}finally {
// 手动清理 ThreadLocal
tl.remove();
}
});
InheritableThreadLocal 与继承性
通过 ThreadLocal 创建的线程变量,其子线程是无法继承的。也就是说你在线程中通过
ThreadLocal 创建了线程变量 V,而后该线程创建了子线程,你在子线程中是无法通过
ThreadLocal 来访问父线程的线程变量 V 的。
如果你需要子线程继承父线程的线程变量,那该怎么办呢?其实很简单,Java 提供了
InheritableThreadLocal 来支持这种特性。
不过,不建议你在线程池中使用 InheritableThreadLocal,不仅仅是因为它具有ThreadLocal 相同的缺点——可能导致内存泄露,更重要的原因是:线程池中线程的创建是动态的,很容易导致继承关系错乱,如果你的业务逻辑依赖 InheritableThreadLocal,那么很可能导致业务逻辑计算错误,而这个错误往往比内存泄露更要命。