Java 中的 ThreadLocal 是如何实现线程资源隔离的?

Sherwin.Wei Lv7

Java 中的 ThreadLocal 是如何实现线程资源隔离的?

回答重点

ThreadLocal 提供了一种线程内独享的变量机制,使每个线程都能有自己独立的变量副本。每个线程内部维护一个 ThreadLocalMap,这个 ThreadLocalMap 用于存储线程独立的变量副本。ThreadLocalMapThreadLocal 实例作为键,以线程独立的变量副本作为值。不同线程通过 ThreadLocal 获取各自的变量副本,而不会影响其他线程的数据。

工作流程

  • 当线程访问 ThreadLocal.get() 时,当前线程会根据自身的 ThreadLocalMap 获取到与调用的 ThreadLocal 对应的值。如果是第一次访问,ThreadLocal 会初始化一个值,并将其存入该线程的 ThreadLocalMap 中。后续访问时,直接从 ThreadLocalMap 中获取,确保每个线程都有自己的数据副本。

扩展知识

ThreadLocal 设计思路理解

大部分人一开始想到 ThreadLocal 来实现线程安全的思路可能就是将 ThreadLocal 看做一个 map ,然后每个线程是 key,这样每个线程去调用 ThreadLocal.get 的时候,将自身作为 key 去 map 找,这样就能获取各自的值了!

听起来很完美?错了!

这样 ThreadLocal 就变成共享变量了,多个线程竞争 ThreadLocal ,那就得保证 ThreadLocal 的并发安全,那就得加锁了,这样绕了一圈就又回去了!

所以这个方案不行,那应该怎么做?

需要在每个线程的本地都存一份值,说白了就是每个线程需要有个变量,来存储这些需要本地化资源的值,并且值有可能有多个,所以怎么弄呢?

在线程对象内部搞个 map,把 ThreadLocal 对象自身作为 key,把它的值作为 map 的值。

这样每个线程可以利用同一个对象作为 key ,去各自的 map 中找到对应的值。

这不就完美了嘛!比如我现在有 3 个 ThreadLocal 对象,2 个线程。

1
2
3
ThreadLocal<String> threadLocal1 =  new ThreadLocal<>();
ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();

那此时 ThreadLocal 对象和线程的关系如下图所示:

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353032302e706e67.png

这样一来就满足了本地化资源的需求,每个线程维护自己的变量,互不干扰,实现了变量的线程隔离,同时也满足存储多个本地变量的需求,完美!

从源码层面分析 Java 的 ThreadLocal 原理

上面我们说到 Thread 对象里面会有个 ThreadLocalMap,用来保存本地变量。

我们来看下 jdk 的 Thread 实现:

1
2
3
4
public class Thread implements Runnable {
// 这就是我们说的那个 map 。
ThreadLocal.ThreadLocalMap threadLocals = null;
}

可以看到,确实有个 map ,不过这个 map 是 ThreadLocal 的静态内部类,记住这个变量的名字 threadLocals,下面会有用的哈。

看到这里,想必有很多小伙伴会产生一个疑问。

竟然这个 map 是放在 Thread 里面使用,那为什么要定义成 ThreadLocal 的静态内部类呢?

首先内部类这个东西是编译层面的概念,就像语法糖一样,经过编译器之后其实内部类会提升为外部顶级类,和平日里外部定义的类没有区别,也就是说在 JVM 中是没有内部类这个概念的。

一般情况下非静态内部类用在内部类,跟其他类无任何关联,专属于这个外部类使用,并且也便于调用外部类的成员变量和方法,比较方便。

而静态外部类其实就等于一个顶级类,可以独立于外部类使用,所以更多的只是表明类结构和命名空间。

所以说这样定义的用意就是说明 ThreadLocalMap 是和 ThreadLocal 强相关的,专用于保存线程本地变量。

现在我们来看一下 ThreadLocalMap 的定义:

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353033362e706e67.png

重点我已经标出来了,首先可以看到这个 ThreadLocalMap 里面有个 Entry 数组,熟悉 HashMap 的小伙伴可能有点感觉了。

这个 Entry 继承了 WeakReference 即弱引用。这里需要注意,不是说 Entry 自己是弱引用,看到我标注的 Entry 构造函数的 super(k) 没,这个 key 才是弱引用。

所以 ThreadLocalMap 里有个 Entry 的数组,这个 Entry 的 key 就是 ThreadLocal 对象,value 就是我们需要保存的值。

那是如何通过 key 在数组中找到 Entry 然后得到 value 的呢 ?

来看下 ThreadLocal 的get 方法,这里就可以得知为什么不同的线程对同一个 ThreadLocal 对象调用 get 方法竟然能得到不同的值了。

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353034362e706e67.png

这个中文注释想必很清晰了吧!ThreadLocal#get 方法首先获取当前线程,然后得到当前线程的 ThreadLocalMap 变量即 threadLocals,然后将自己作为 key 从 ThreadLocalMap 中找到 Entry ,最终返回 Entry 里面的 value 值。

这里我们再看一下 key 是如何从 ThreadLocalMap 中找到 Entry 的,即map.getEntry(this)是如何实现的,其实很简单。

image.png

可以看到 ThreadLocalMap 虽然和 HashMap 一样,都是基于数组实现的,但是它们对于 Hash 冲突的解决方法不一样,HashMap 是通过链表(红黑树)法来解决冲突,而 ThreadLocalMap 是通过开放寻址法来解决冲突。

听起来好像很高级,其实道理很简单,我们来看一张图就很清晰了。

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353130392e706e67.png

所以说,如果通过 key 的哈希值得到的下标无法直接命中,则会将下标 +1,即继续往后遍历数组查找 Entry ,直到找到或者返回 null。

可以看到,这种 hash 冲突的解决效率其实不高,但是一般 ThreadLocal 也不会太多,所以用这种简单的办法解决即可。

至于代码中的 expungeStaleEntry 我们等下再分析,先来看下 ThreadLocalMap#set 方法,看看写入的怎样实现的,来看看 hash 冲突的解决方法是否和上面说的一致。

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353131392e706e67.png

可以看到 set 的逻辑也很清晰,先通过 key 的 hash 值计算出一个数组下标,然后看看这个下标是否被占用了,如果被占了看看是否就是要找的 Entry ,如果是则进行更新,如果不是则下标++,即往后遍历数组,查找下一个位置,找到空位就 new 个 Entry 然后把坑给占用了。

当然,这种数组操作一般免不了阈值的判断,如果超过阈值则需要进行扩容。

上面的清理操作和 key 为空的情况,下面再做分析,这里先略过。

至此,我们已经分析了 ThreadLocalMap 的核心操作 get 和 set ,想必你对 ThreadLocalMap 的原理已经从源码层面清晰了!

可能有些小伙伴对 key 的哈希值的来源有点疑惑,所以我再来补充一下 key.threadLocalHashCode的分析。

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353132382e706e67.png

可以看到 key.threadLocalHashCode 其实就是调用 nextHashCode 进行一个原子类的累加。

注意看上面都是静态变量和静态方法,所以在 ThreadLocal 对象之间是共享的,然后通过固定累加一个奇怪的数字0x61c88647来分配 hash 值。

这个数字当然不是乱写的,是实验证明的一个值,即通过 0x61c88647 累加生成的值与 2 的幂取模的结果,可以较为均匀地分布在 2 的幂长度的数组中,这样可以减少 hash 冲突。

Comments