ThreadLocal 的深析
ThreadLocal 是什么
ThreadLocal 提供线程局部变量,这些变量与普通变量的不同之处在于,访问这些变量的每个线程(通过 ThreadLocal 的 get 或 set 方法)都有自己的、独立初始化的变量副本。
ThreadLocal 实例通常指代希望与线程关联的类中的私有静态字段。例如,我希望为每个线程分配一个 uuid,我会这么写:
private static final ThreadLocal<UUID> threadId =
new ThreadLocal<UUID>() {
@Override protected UUID initialValue() {
return UUID.randomUUID();
}
};
或者:
private static final ThreadLocal<UUID> threadId =
ThreadLocal.withInitial(UUID::randomUUID);
在上例中,如果某个线程第一次通过 threadId.get()
获取 uuid,则会触发初始化分配一个独立的 uuid,不同的线程无法访问对方的 uuid。
ThreadLocal 如何实现的?
ThreadLocal 的 get() 方法本质上是通过哈希表的方式获取线程隔离的数据,但它不是直接使用 HashMap,而是依赖储存在每个线程内部的特殊数据结构 —— ThreadLocalMap,这是一个以 ThreadLocal 为 key(弱引用)、实际数值为 value(强引用)的哈希表数据结构。
ThreadLocal 实例本身只是字段的指代,并不储存实际值。因此不需要对它使用 volatile 关键字。
以下流程图展示了 ThreadLocal.get() 调用后的大致流程:
为什么使用 ThreadLocal?
在上文中,我们用 ThreadLocal 给每个线程分配了独立的 uuid。如果我们不使用 ThreadLocal,而是使用一个 ConcurrentHashMap,虽然节约了空间,但是产生了加锁的开销。
一些 ThreadLocal 的应用场景:
- 在使用数据库连接池时,可以通过 ThreadLocal 为每个线程绑定一个独立的数据库连接。
- 同线程多流程业务中,将上下文信息存于 ThreadLocal 中,避免使用公共资源。
ThreadLocal 内存泄漏问题
ThreadLocalMap 可能会发生内存泄露,尤其是在使用线程池时。ThreadLocalMap 对 ThreadLocal 是弱引用,但对值本身是强引用。如果线程消亡,垃圾回收时会自动回收该线程的 ThreadLocalMap,不会发生内存泄露。但是线程池中的线程不会消亡,其中,如果对于键的引用被回收,值将无法被访问,也无法解除引用,这就发生了内存泄露。
为避免这种问题,要在多线程业务结束时显式调用 ThreadLocal.clear();
。