type
status
date
slug
summary
tags
category
icon
password

1、概述

有时候我们想在一个线程里面传递状态信息,这些状态信息只在本线程可见,其他线程不可见,需要传递的对象,我们通常称为上下文(Context)。ThreadLocal 是 Java 标准库提供的可以实现线程上下文传递的工具,它可以在一个线程中传递同一个对象。
ThreadLocal 是一个将在多线程中为每一个线程创建单独的变量副本的类,当使用 ThreadLocal 来维护变量时,ThreadLocal 会为每个线程创建单独的变量副本,避免因多线程操作共享变量而导致的数据不一致的情况。ThreadLocal实例通常以静态字段初始化如下:
常用的应用场景是在数据库连接的时候为每个线程保存一个当前线程可见的连接。这样在 Client 获取 Connection 的时候,每个线程获取到的 Connection 都是该线程独有的,做到 Connection 的线程隔离,所以并不存在线程安全问题。

2、ThreadLocal原理

2.1 JDK7的设计

ThreadLocal 的早期设计,每个 ThreadLocal 都创建一个 Map,然后用线程作为 Map 的 Key,要存储的局部变量作为 Map 的 value,这样就能达到各个线程的局部变量隔离的效果。
但这种设计有一个问题,如果调用线程一直不终止,那么这个本地变量会一直存放在 ThreadLocal 变量里面,容易造成内存泄漏,解决办法是当不需要使用本地变量时可以通过调用 ThreadLocal 变量的 remove 方法。
notion image

2.2 JDK8的设计

JDK后面优化了设计方案,在 JDK8 中 ThreadLocal 的设计是:
  • 每个 Thread 线程内部都有一个 ThreadLocalMap 类型的变量 threadLocals
  • ThreadLocalMap 没有继承 Map 接口,而是自定义 Entry 类。Entry 的 key 是 Threadlocal 对象的弱引用,value 是真正要存储的值,使用数组 Entry[] 去实现。所以每个线程可以关联多个 ThreadLocal 变量。
  • ThreadLocal 通过 set 方法把 value 值放入调用线程的 threadLocals 变量里面并存放起来,当调用线程调用它的 get方法时,再从当前线程的 threadLocals 变量里面将其拿出来使用。
  • 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
notion image
这种方案设计的好处:
  • 每个 Map 存储的 Entry 数量变少。因为之前的存储数量由 Thread 的数量决定,现在是由 ThreadLocal 的数量决定。在实际开发中,ThreadLocal 的数量往往少于线程的数量。数量变少也可以尽量避免哈希冲突。
  • 当 Thread 销毁的时候,ThreadLocalMap 也会随之销毁,减少内存的使用。

3、ThreadLocal常见问题

3.1 ThreadLocal真的只对当前线程可见吗?

ThreadLocal 的实例仍然是存储在 java 堆上,而不是存放在栈上。只是通过了特殊的方式只让当前线程可见。
我们也可以通过特殊的方式,使用 InheritableThreadLocal 可以实现多个线程访问 ThreadLocal 的值。例如我们在主线程中创建一个 InheritableThreadLocal 的实例,然后在子线程中得到这个 InheritableThreadLocal 实例设置的值。

3.2 ThreadLocal变量为什么通常使用static修饰?

根据 https://www.zhihu.com/question/35250439 的回答,结论是为了避免重复创建TSO(Thread Specific Object,即ThreadLocal所关联的对象)所导致的浪费。
我们知道,一个 ThreadLocal 实例对应当前线程中的一个 TSO 实例。因此,如果把 ThreadLocal 声明为某个类的实例变量(而不是静态变量),那么每创建一个该类的实例就会导致一个新的TSO实例被创建。显然,这些被创建的 TSO 实例是同一个类的实例。于是,同一个线程可能会访问到同一个 TSO(指类)的不同实例,这即便不会导致错误,也会导致浪费(重复创建等同的对象)!因此,一般我们将 ThreadLocal 使用 static 修饰即可。例如,
但是 ThreadLocal 使用 static 修饰之后,ThreadLocal 实例的生命周期和类相同,这可能会导致内存泄漏的问题,详见下一点。

3.3 ThreadLocal是否存在内存泄漏问题?

先说结论:ThreadLocal 有可能会出现内存泄漏的问题。
内存泄漏指的是,当某一个对象不再有用的时候,占用的内存却不能被回收,这就叫作内存泄漏。因为通常情况下,如果一个对象不再有用,那么我们的垃圾回收器 GC,就应该把这部分内存给清理掉。这样的话,就可以让这部分内存后续重新分配到其他的地方去使用;否则,如果对象没有用,但一直不能被回收,这样的垃圾对象如果积累的越来越多,则会导致我们可用的内存越来越少,最后发生内存不够用的 OOM 错误。
在 JDK8 的实现里面,每一个 Thread 都有一个 ThreadLocal.ThreadLocalMap 这样的类型变量,该变量的名字叫作 threadLocals。线程在访问了 ThreadLocal 之后,都会在它的 ThreadLocalMap 里面的 Entry 中去维护该 ThreadLocal 变量与具体实例的映射。下面我们基于 JDK8 的实现分析一下 ThreadLocal 是否存在内存泄漏的问题。

3.3.1 Key的泄漏

结论:Key 不存在内存泄漏的问题。
我们可能会在业务代码中执行了 ThreadLocal instance = null 操作,想清理掉这个 ThreadLocal 实例,但是假设我们在 ThreadLocalMap 的 Entry 中强引用了 ThreadLocal 实例,那么,虽然在业务代码中把 ThreadLocal 实例置为了 null,但是在 Thread 类中依然有这个引用链的存在。
GC 在垃圾回收的时候会进行可达性分析,它会发现这个 ThreadLocal 对象依然是可达的,所以对于这个 ThreadLocal 对象不会进行垃圾回收,这样的话就造成了内存泄漏的情况。
JDK 开发者考虑到了这一点,所以 ThreadLocalMap 中的 Entry 继承了 WeakReference 弱引用,代码如下所示
这个 Entry 是 extends WeakReference。弱引用的特点是,如果这个对象只被弱引用关联,而没有任何强引用关联,那么这个对象就可以被回收,所以弱引用不会阻止 GC。因此,这个弱引用的机制就避免了 ThreadLocal 的内存泄露问题。
所以 ThreadLocal 没有被外部强引用的情况下(例如执行了ThreadLocal instance = null 操作),在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap 中使用这个 ThreadLocal 的 key 也会被清理掉,所以 key 不会发生内存泄漏的问题。

3.3.2 Value的泄漏

结论:Value可能存在内存泄漏的问题。
虽然 ThreadLocalMap 的每个 Entry 都是一个对 key 的弱引用,但是这个 Entry 包含了一个对 value 的强引用,还是刚才那段代码
可以看到,value = v 这行代码就代表了强引用的发生
notion image
可以看到,左侧是引用栈,栈里面有一个 ThreadLocal 的引用和一个线程的引用,右侧是我们的堆,在堆中是对象的实例。
重点看一下下面这条链路: Thread Ref → Current Thread → ThreadLocalMap → Entry → Value → 可能泄漏的value实例
这条链路是随着线程的存在而一直存在的,如果线程执行耗时任务而不停止,那么当垃圾回收进行可达性分析的时候,这个 Value 就是可达的,所以不会被回收。但是与此同时可能我们已经完成了业务逻辑处理,不再需要这个 Value 了,此时也就发生了内存泄漏问题。
JDK 同样也考虑到了这个问题,在执行 ThreadLocal 的 set、remove、rehash 等方法时,它都会扫描 key 为 null 的 Entry,如果发现某个 Entry 的 key 为 null,则代表它所对应的 value 也没有作用了,所以它就会把对应的 value 置为 null,这样,value 对象就可以被正常回收了。
但是假设 ThreadLocal 已经不被使用了,那么实际上 set、remove、rehash 方法也不会被调用,与此同时,如果这个线程又一直存活、不终止的话,那么刚才的那个调用链就一直存在,也就导致了 value 的内存泄漏。

3.3.3 如何避免内存泄露

调用 ThreadLocal 的 remove 方法。调用这个方法就可以删除对应的 value 对象,可以避免内存泄漏。
它是先获取到 ThreadLocalMap 这个引用的,并且调用了它的 remove 方法。这里的 remove 方法可以把 key 所对应的 value 给清理掉,这样一来,value 就可以被 GC 回收了,在使用完了 ThreadLocal 之后,我们应该手动去调用它的 remove 方法,目的是防止内存泄漏的发生。
Kafka系列:分区机制Kafka系列:Kafka入门
mcbilla
mcbilla
一个普通的干饭人🍚
Announcement
type
status
date
slug
summary
tags
category
icon
password
🎉欢迎来到飙戈的博客🎉
-- 感谢您的支持 ---
👏欢迎学习交流👏