探秘 ThreadLocal 的实现机制与小地雷
Java 多线程类库对于共享数据的读写访问主要采用同步机制来保证线程安全,而本文所要探究的 ThreadLocal 则采用了一种完全不同的策略,它不是用来解决共享数据的并发访问问题的,ThreadLocal 让每个线程都将目标数据复制一份作为线程私有,后续对于该数据的操作都是在各自私有的副本上进行,线程之间彼此相互隔离,也就不存在竞争问题。
下面的例子演示了 ThreadLocal 的典型应用场景。在 jdk 1.8 之前,如果我们希望对日期和时间进行格式化操作,则需要使用 SimpleDateFormat 类,而我们知道它是是线程不安全的,在多线程并发执行时会出现一些奇怪的问题。对于该类使用的最佳实践则是采用 ThreadLocal 进行包装,以保证每个线程都有一份属于自己的 SimpleDateFormat 对象,如下所示:
1 | ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>() { |
实现机制
那么 ThreadLocal 是怎么做到让修饰的对象能够在每个线程中各自持有一份呢?我们先来从整体的角度简单概括一下。
在 ThreadLocal 中定义了一个静态内部类 ThreadLocalMap,可以将其理解为一个特有的 Map 类型,而在 Thread 类中声明了一个 ThreadLocalMap 类型的 threadLocals 属性。针对每个 Thread 对象,也就是每个线程来说都包含了一个 ThreadLocalMap 对象,即每个线程都有一个属于自己的内存数据库,而数据库中存储的就是我们用 ThreadLocal 修饰的对象。整个过程还是有点绕的,可以借助下面这幅图进行理解:
这里的 key 就是对应的 ThreadLocal 对象自身,而 value 就是 ThreadLocal 修饰的属性值。当希望获取该对象时,我们首先需要拿到当前线程对应的 Thread 对象,然后获取到该对象对应的 threadLocals 属性,也就拿到了线程私有的内存数据库,最后以 ThreadLocal 对象为 key 获取到其修饰的目标值。
线程内存数据库
接下来看一下相应的源码实现,首先来看一下内部定义的 ThreadLocalMap 静态内部类:
1 | static class ThreadLocalMap { |
ThreadLocalMap 是一个定制化的 Map 实现,可以简单将其理解为一般的 Map,用作键值存储的内存数据库,至于为什么要专门实现而不是复用已有的 HashMap,我们在后面进行说明。
API 实现分析
了解了 ThreadLocalMap 的定义,我们再来看一下 ThreadLocal 的实现。对于 ThreadLocal 来说,对外暴露的方法主要有 get、set,以及 remove 三个,下面逐一展开分析。
获取线程私有值
与一般的 Map 取值操作不同,这里的 ThreadLocal#get
方法并没有要求提供查询的 key,也正如前面所说的,这里的 key 就是调用 ThreadLocal#get
方法的 ThreadLocal 对象自身:
1 | public T get() { |
如果当前线程对应的内存数据库 map 对象还未创建,则会调用 ThreadLocal#setInitialValue
方法执行创建,如果在构造 ThreadLocal 对象时覆盖实现了 ThreadLocal#initialValue
方法,则会调用该方法获取构造的初始化值并记录到创建的 map 对象中:
1 | private T setInitialValue() { |
设置线程私有值
再来看一下 ThreadLocal#set
方法,因为 key 就是当前 ThreadLocal 对象,所以 ThreadLocal#set
方法也不需要指定 key:
1 | public void set(T value) { |
和 ThreadLocal#get
方法的流程大致一样,都是操作当前线程私有的内存数据库 ThreadLocalMap,并记录目标值。
删除线程私有值
方法 ThreadLocal#remove
以当前 ThreadLocal 对象为 key,从当前线程内存数据库 ThreadLocalMap 中删除目标值,具体逻辑比较简单:
1 | public void remove() { |
ThreadLocal 对外暴露的功能虽然有点小神奇,但是具体对应到内部实现并没有什么复杂的逻辑。如果我们把每个线程持有的专属 ThreadLocalMap 对象理解为当前线程的私有数据库,那么也就不难理解 ThreadLocal 的运行机制。每个线程自己维护自己的数据,彼此相互隔离,不存在竞争,也就没有线程安全问题可言。
真的就高枕无忧了吗
虽然对于每个线程来说数据是隔离的,但这也不表示任何对象丢到 ThreadLocal 中就万事大吉了,思考一下下面几种情况:
- 如果记录在 ThreadLocal 中的是一个线程共享的外部对象呢?
- 引入线程池,情况又会有什么变化?
- 如果 ThreadLocal 被 static 关键字修饰呢?
先来看 第 1 个问题 ,如果我们记录的是一个外部线程共享的对象,虽然我们以当前线程私有的 ThreadLocal 对象作为 key 对其进行了存储,但是恶魔终究是恶魔,共享的本质并不会因此而改变,这种情况下的访问还是需要进行同步控制,最好的方法就是从源头屏蔽掉这类问题。我们来举个例子:
1 | public class ThreadLocalWithSharedInstance implements Runnable { |
以上程序最终的输出如下:
1 | [Thread-a], list=[a_2, a_7, a_4, a_5, a_7] |
可以看到虽然使用了 ThreadLocal 修饰,但是 list 还是以共享的方式在多个线程之间被访问,如果不加控制则会存在线程安全问题。
再来看 第 2 个问题 ,相对问题 1 来说引入线程池就更加可怕,因为大部分时候我们都不会意识到问题的存在,直到代码暴露出奇怪的现象。这一场景并没有违背线程私有的本质,只是一个线程被复用来处理多个业务,而这个被线程私有的对象也会在多个业务之间被共享。例如:
1 | public class ThreadLocalWithThreadPool implements Callable<Boolean> { |
以上程序的最终输出如下:
1 | cpu core size : 8 |
示例中,我用一个大小为 2 的线程池进行了模拟,可以看到初始化方法被调用了两次,所有线程的操作都是复用这两个线程。
回忆一下前文所说的,ThreadLocal 的本质就是为每个线程维护一个线程私有的内存数据库来记录线程私有的对象,但是在线程池情况下线程是会被复用的,也就是说线程私有的内存数据库也会被复用,如果在一个线程被使用完准备回放到线程池中之前,我们没有对记录在数据库中的数据执行清理,那么这部分数据就会被下一个复用该线程的业务看到,从而间接的共享了该部分数据。
最后我们再来看一下 第 3 个问题 ,我们尝试将 ThreadLocal 对象用 static 关键字进行修饰:
1 | public class ThreadLocalWithStaticEmbellish implements Runnable { |
以上程序的最终输出如下:
1 | thread-a init thread local |
由程序运行结果可以看到 static 修饰并没有引出什么问题,实际上这也是很容易理解的,ThreadLocal 采用 static 修饰仅仅是让数据库中记录的 key 是一样的,但是每个线程的内存数据库还是私有的,并没有被共享,就像不同的公司都有自己的用户信息表,即使一些公司之间的用户 ID 是一样的,但是对应的用户数据却是完全隔离的。
以上例子演示了一开始抛出的 3 个问题,其中问题 1 和问题 2 都是 ThreadLocal 使用过程中的小地雷。例子举的不一定恰当,实际中可能也不一定会如示例中这样去使用 ThreadLocal,主要还是为了传达一些意识。如果明白了 ThreadLocal 的内部实现细节,就能够很自然的绕过这些小地雷。
真的会内存泄露吗
关于 ThreadLocal 导致内存泄露的问题,曾经有一段时间在网上争得沸沸扬扬,那么到底会不会导致内存泄露呢?这里先给出答案:
如果使用不恰当,存在内存泄露的可能性。
我们来分析一下内存泄露的条件和原因,在最开始看 ThreadLocal 源码的时候,我就有一个疑问,ThreadLocal 为什么要专门实现 ThreadLocalMap,而不是采用已有的 HashMap 代替 ?
后来分析具体实现时看到执行存储时的 key 为当前 ThreadLocal 对象,不需要专门指定 key 能够在一定程度上简化使用,但这并不足以为此专门去实现 ThreadLocalMap。继续阅读我发现 ThreadLocalMap 在实现 Entry 的时候有些奇怪,居然继承了 WeakReference:
1 | static class Entry extends WeakReference<ThreadLocal<?>> { |
从而让 key 成为一个弱引用,我们知道弱引用对象拥有非常短暂的生命周期,在垃圾收集器线程扫描其所管辖的内存区域过程中,一旦发现了弱引用对象,不管当前内存空间是否足够都会回收它的内存。也就是说这样的设计会很容易导致 ThreadLocal 对象被回收,线程所执行任务的时间长度是不固定的,这样的设计能够方便垃圾收集器回收线程私有的变量。
由此可以看出作者这样设计的目的是为了防止内存泄露,那怎么就变成了被很多文章所分析的是内存泄漏的导火索呢?这些文章的共同观点就是 key 被回收了,但是 value 是一个强引用没有被回收,这些 value 就变成了一个个的僵尸。这样的分析没有错,value 确实存在,且和线程是同生命周期的,但是如下策略可以保证尽量避免内存泄露:
- ThreadLocal 在每次执行 get 和 set 操作的时候都会去清理 key 为 null 的 value 值。
- value 与线程同生命周期,线程死亡之时,也是 value 被 GC 之日。
策略 1 没啥好说的,看看源码就知道,我们来举例验证一下策略 2:
1 | public class ThreadLocalWithMemoryLeak implements Callable<Boolean> { |
以上程序的最终输出如下:
1 | Thread-11 is running |
可以看到 value 最终还是被 GC 了,虽然第 1 次 GC 的时候没有被回收,这也验证 value 和线程是同生命周期的,之所以示例中等待 60 秒是因为 Executors#newCachedThreadPool
中的线程默认生命周期是 60 秒,如果生命周期内该线程没有被再次复用则会死亡,我们这里就是要等待线程死亡,一但线程死亡,value 也就被 GC 了。
所以 出现内存泄露的前提必须是持有 value 的线程一直存活 ,这在使用线程池时是很正常的,在这种情况下 value 一直不会被 GC,因为线程对象与 value 之间维护的是强引用。此外就是 后续线程执行的业务一直没有调用 ThreadLocal 的 get 或 set 方法,导致不会主动去删除 key 为 null 的 value 对象 ,在满足这两个条件下 value 对象一直常驻内存,所以存在内存泄露的可能性。
那么我们应该怎么避免呢?前面我们分析过线程池情况下使用 ThreadLocal 存在小地雷,这里的内存泄露一般也都是发生在线程池的情况下,所以在使用 ThreadLocal 时,对于不再有效的 value 主动调用一下 remove 方法来进行清除,从而消除隐患,这也算是最佳实践吧。
InheritableThreadLocal 又是什么鬼
InheritableThreadLocal 继承自 ThreadLocal,实现上也比较简单(如下),那么 InheritableThreadLocal 与 ThreadLocal 到底有什么区别呢?
1 | public class InheritableThreadLocal<T> extends ThreadLocal<T> { |
在开始分析之前,我们先演示一个 ThreadLocal 的案例,如下:
1 | private static ThreadLocal<String> tl = new ThreadLocal<>(); |
运行上述示例,输出如下:
1 | Main thread: zhenchao |
可以看出,子线程拿不到主线程设置的 ThreadLocal 变量,当然这也是可以理解的,毕竟主线程和子线程之间仍然是两个线程,但是在一些场景下我们希望对于主线程和子线程这种关系而言,ThreadLocal 变量能够被继承。这个时候就可以使用 InheritableThreadLocal 来实现,对于上述示例而言,只需要将 ThreadLocal 改为 InheritableThreadLocal 即可,具体实现比较简单,读者可以自己尝试一下。
下面我们来分析一下 InheritableThreadLocal 是如何做到让 ThreadLocal 变量在主线程和子线程之间进行继承的。由 InheritableThreadLocal 的实现来看,InheritableThreadLocal 使用了 inheritableThreadLocals 变量替换了 ThreadLocal 的 threadLocals 变量,而这两个变量都是 ThreadLocalMap 类型。子线程在初始化时会判断父线程的 inheritableThreadLocals 是否为 null,如果不为 null,则使用父类的 inheritableThreadLocals 变量初始化自己的 inheritableThreadLocals,实现如下(位于 Thread#init
方法中):
1 | // 如果父线程的 inheritableThreadLocals 变量不为空,则复制给子线程 |
而 ThreadLocal#createInheritedMap
的实现如下:
1 | static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { |
方法 InheritableThreadLocal#childValue
的实现只是简单返回了父线程中的值,所以上述过程本质上就是一个拷贝父线程中 ThreadLocal 变量值的过程。
由上述实现我们可以看到,父线程和子线程在 ThreadLocal 变量的存储上仍然是隔离的,只是在初始化子线程时会拷贝父线程的 ThreadLocal 变量,之后在运行期间彼此互不干涉,也就是说在子线程启动起来之后,父线程和子线程各自对同一个 InheritableThreadLocal 实例的改动并不会被对方所看见。
总结
本文我们分析了 ThreadLocal 的实现,以及存在的一些小地雷,并讨论了在什么情况下会造成内存泄漏,最后还分析了与 ThreadLocal 师出同宗的 InheritableThreadLocal 类。ThreadLocal 和 InheritableThreadLocal 在保证线程安全性方面算是另辟蹊径,能够在一些场景下简化多线程编程,是 java 程序员必须掌握的一部分。当然,理解其实现原理能够帮助我们更好的使用其特性,避免不经意踩到小地雷。