ThreadLocal 的使用方法

下面这个静态类 ThreadId 会为每个线程分配一个唯一的线程 Id,如果一个线程前后两次调用 ThreadId 的 get() 方法,两次 get() 方法的返回值是相同的。但如果是两个线程分别调用 ThreadId 的 get() 方法,那么两个线程看到的 get() 方法的返回值是不同的。

1
2
3
4
5
6
7
8
9
static class ThreadId {
static final AtomicLong nextId = new AtomicLong(0);
// 定义ThreadLocal变量
static final ThreadLocal<Long> tl = ThreadLocal.withInitial(() -> nextId.getAndIncrement());
//此方法会为每个线程分配一个唯一的Id
static long get() {
return tl.get();
}
}

一个使用场景:

SimpleDateFormat 不是线程安全的,那如果需要在并发场景下使用它,就可以使用ThreadLocal 来解决。不同线程调用 SafeDateFormat 的 get() 方法将返回不同的 SimpleDateFormat 对象实例,由于不同线程并不共享 SimpleDateFormat,所以就像局部变量一样,是线程安全的。

1
2
3
4
5
6
7
8
9
10
11
12
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 的工作原理

Java对于ThreadLocal 的实现,也有一个Map,叫做 ThreadLocalMap,不过持有 ThreadLocalMap 的不是 ThreadLocal,而是 Thread。Thread 这个类内部有一个私有属性 threadLocals,其类型就是 ThreadLocalMap,ThreadLocalMap 的 Key 是 ThreadLocal。如下图所示:

image-20230523162923334

另一种设计方案

如果我们仅仅是简单的用一个Map来保存,即Map的key是线程id,value是每个线程拥有的变量V。这样会导致一个问题,那就是线程的id一直被引用,导致线程资源无法被回收。

ThreadLocal 与内存泄露

在线程池中使用 ThreadLocal 为什么可能导致内存泄露呢?原因就出在线程池中线程的存活时间太长,往往都是和程序同生共死的,这就意味着 Thread 持有的 ThreadLocalMap 一直都不会被回收,再加上 ThreadLocalMap 中的 Entry 对 ThreadLocal 是弱引用(WeakReference),所以只要 ThreadLocal 结束了自己的生命周期是可以被回收掉的。但是 Entry 中的 Value 却是被 Entry 强引用的,所以即便 Value 的生命周期结束了,Value 也是无法被回收的,从而导致内存泄露。

所以我们要手动释放,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
ExecutorService es;
ThreadLocal tl;
es.execute(()->{
//ThreadLocal增加变量
tl.set(obj);
try {
// 省略业务逻辑代码
} finally {
//手动清理ThreadLocal
tl.remove();
}
});