一, 前言
这篇博客来分析一下 ThreadLocal 的实现原理以及常见问题, 由于现在时间比较晚了, 我就不废话了, 直接进入正题.
二, 正文
2.1 ThreadLocal 是什么
在讲实现原理之前, 我先来简单的说一说 ThreadLocal 是什么. ThreadLocal 被称作线程局部变量, 当我们定义了一个 ThreadLocal 变量, 所有的线程共同使用这个变量, 但是对于每一个线程来说, 实际操作的值是互相独立的. 简单来说就是, ThreadLocal 能让线程拥有自己内部独享的变量. 举一个简单的例子:
- // 定义一个线程共享的 ThreadLocal 变量
- static ThreadLocal<Integer> tl = new ThreadLocal<>();
- public static void main(String[] args) {
- // 创建第一个线程
- Thread t1 = new Thread(() -> {
- // 设置 ThreadLocal 变量的初始值, 为 1
- tl.set(1);
- // 循环打印 ThreadLocal 变量的值
- for (int i = 0; i <10; i++) {
- System.out.println(Thread.currentThread().getName() + "----" + tl.get());
- // 每次打印完让值 + 1
- tl.set(tl.get() + 1);
- }
- }, "thread1");
- // 创建第二个线程
- Thread t2 = new Thread(() -> {
- // 设置 ThreadLocal 变量的初始值, 为 100, 与上一个线程区别开
- tl.set(100);
- // 循环打印 ThreadLocal 变量的值
- for (int i = 0; i <10; i++) {
- System.out.println(Thread.currentThread().getName() + "----" + tl.get());
- // 每次打印完让值 - 1
- tl.set(tl.get() - 1);
- }
- }, "thread2");
- // 开启两个线程
- t1.start();
- t2.start();
- tl.remove();
- }
上面的代码, 运行结果如下 (注: 每次运行的结果可能不同):
- thread1----1
- thread2----100
- thread1----2
- thread2----99
- thread1----3
- thread2----98
- thread1----4
- thread2----97
- thread1----5
- thread2----96
- thread1----6
- thread2----95
- thread1----7
- thread2----94
- thread1----8
- thread2----93
- thread1----9
- thread2----92
- thread1----10
- thread2----91
通过上面的输出结果我们可以发现, 线程 1 和线程 2 虽然使用的是同一个 ThreadLocal 变量存储值, 但是输出结果中, 两个线程的值却互不影响, 线程 1 从 1 输出到 10, 而线程 2 从 100 输出到 91. 这就是 ThreadLocal 的功能, 即让每一个线程拥有自己独立的变量, 多个线程之间互不影响.
2.2 ThreadLocal 的实现原理
下面我就就来说一说 ThreadLocal 是如何做到线程之间相互独立的, 也就是它的实现原理. 这里我直接放出结论, 后面再根据源码分析: 每一个线程都有一个对应的 Thread 对象, 而 Thread 类有一个成员变量, 它是一个 Map 集合, 这个 Map 集合的 key 就是 ThreadLocal 的引用, 而 value 就是当前线程在 key 所对应的 ThreadLocal 中存储的值. 当某个线程需要获取存储在 ThreadLocal 变量中的值时, ThreadLocal 底层会获取当前线程的 Thread 对象中的 Map 集合, 然后以 ThreadLocal 作为 key, 从 Map 集合中查找 value 值. 这就是 ThreadLocal 实现线程独立的原理. 也就是说, ThreadLocal 能够做到线程独立, 是因为值并不存在 ThreadLocal 中, 而是存储在线程对象中. 下面我们根据 ThreadLocal 中两个最重要的方法来确认这一点.
2.3 ThreadLocal 中的 get 方法
get 方法的作用非常简单, 就是线程向 ThreadLocal 中取值, 下面我们来看看它的源码:
- public T get() {
- // 获取当前线程的 Thread 对象
- Thread t = Thread.currentThread();
- // getMap 方法传入 Thread 对象, 此方法将返回 Thread 对象中存储的一个 Map 集合
- // 这个 Map 集合的类型为 ThreadLocalMap, 这是 ThreadLoacl 的一个内部类
- // 当前线程存放在 ThreadLocal 中的值, 实际上存放在这个 Map 集合中
- ThreadLocalMap map = getMap(t);
- // 如果当前 Map 集合已经初始化, 则直接从 Map 集合中查找
- if (map != null) {
- // ThreadLocalMap 的 key 其实就是 ThreadLoacl 对象的引用
- // 所以要找到线程在当前 ThreadLoacl 中存放的值, 就需要以当前 ThreadLoacl 作为 key
- // getEntry 方法就是通过 key 获取 map 中的一个 key-value, 而这里使用的 key 就是 this
- ThreadLocalMap.Entry e = map.getEntry(this);
- // 如果返回值不为空, 表示查找成功
- if (e != null) {
- @SuppressWarnings("unchecked")
- // 于是获取对应的 value 并返回
- T result = (T)e.value;
- return result;
- }
- }
- // 若当前线程的 ThreadLocalMap 还未初始化, 或者查找失败, 则调用以下方法
- return setInitialValue();
- }
- private T setInitialValue() {
- // 此方法默认返回 null, 但是可以由子类进行重新, 根据需求返回需要的值
- T value = initialValue();
- // 获取当前线程的 Thread 对象
- Thread t = Thread.currentThread();
- // 获取对应的 ThreadLocalMap
- ThreadLocalMap map = getMap(t);
- // 如果 Map 已经初始化了, 就直接往 map 中加入一个 key-value
- // key 就是当前 ThreadLocal 对象的引用, 而 value 就是上面获取到的 value, 默认为 null
- if (map != null)
- map.set(this, value);
- // 若还没有初始化, 则调用 createMap 创建 ThreadLocalMap 对象
- else
- createMap(t, value);
- // 返回 initialValue 方法返回的值, 默认为 null
- return value;
- }
- void createMap(Thread t, T firstValue) {
- // 创建 ThreadLocalMap 对象, 构造方法传入的是第一对放入其中的 key-value
- // 这个 key 也就是当前线程第一次调用 get 方法的 ThreadLocal 对象, 也就是当前 ThreadLocal 对象
- // 而 firstValue 则是 initialValue 方法的返回值, 默认为 null
- t.threadLocals = new ThreadLocalMap(this, firstValue);
- }
上面的代码非常直观的验证了我之前说过的 ThreadLocal 的实现原理. 通过上面的代码, 我们可以非常直观的看到, 线程向 ThreadLocal 中存放的值, 最后都放入了线程自己的 ThreadLocalMap 中, 而这个 map 的 key 就是当前 ThreadLocal 的引用. 而 ThreadLocal 中, 获取线程的 ThreadLocalMap 的方法 getMap 的代码如下:
- ThreadLocalMap getMap(Thread t) {
- // 直接返回 Thread 对象的 threadLocals 成员变量
- return t.threadLocals;
- }
我们再看看 Thread 类中的 threadLocals 变量:
- /** 可以看到, ThreadLocalMap 是 ThreadLocal 的内部类 */
- ThreadLocal.ThreadLocalMap threadLocals = null;
2.4 ThreadLocal 中的 set 方法
下面再来看一看 ThreadLocal 的 set 方法的实现, set 方法用来使线程向 ThreadLocal 中存放值 (实际上是存放在线程自己的 Map 中):
- public void set(T value) {
- // 获取当前线程的 Thread 对象
- Thread t = Thread.currentThread();
- // 获取当前线程的 ThreadLocalMap
- ThreadLocalMap map = getMap(t);
- // 若 map 已经初始化, 则之际将 value 放入 Map 中, 对应的 key 就是当前 ThreadLocal 的引用
- if (map != null)
- map.set(this, value);
- // 若没有初始化, 则调用 createMap 方法, 为当前线程 t 创建 ThreadLocalMap,
- // 然后将 key-value 放入 (此方法已经在上面讲解 get 方法是看过)
- else
- createMap(t, value);
- }
这就是 set 方法的实现, 比较简单. 看完上面两个关键方法的实现, 相信大家对 ThreadLocal 的实现已经有了一个比较清晰的认识, 下面我们来更加深入的分析 ThreadLocal, 看看 ThreadLocalMap 的一些实现细节.
2.5 ThreadLocalMap 的中的弱引用
ThreadLocalMap 的实现其实就是一个比较普通的 Map 集合, 它的实现和 HashMap 类似, 所以具体的实现细节我们就不一一讲解了, 这里我们只关注它最特别的一个地方, 即它内部的节点 Entry. 我们先来看看 Entry 的代码:
- // Entry 是 ThreadLocalMap 的内部类, 表示 Map 的节点
- // 这里继承了 WeakReference, 这是 java 实现的弱引用类, 泛型为 ThreadLocal
- // 表示在这个 Map 中, 作为 key 的 ThreadLocal 是弱引用
- // (这里 value 是强引用, 因为没用 WeakReference)
- static class Entry extends WeakReference<ThreadLocal<?>> {
- /** 存储 value */
- Object value;
- Entry(ThreadLocal<?> k, Object v) {
- // 将 key 的值传入父类 WeakReference 的构造方法, 用弱引用来引用 key
- super(k);
- // value 则直接使用上面的强引用
- value = v;
- }
- }
可以看到, 上面的 Entry 比较特殊, 它继承自 WeakReference 类型, 这是 Java 实现的弱引用. 在具体讲解前, 我们先来介绍一下不同类型的引用:
强引用: 这是 Java 中最常见的引用, 在没有使用特殊引用的情况下, 都是强引用, 比如 Object o = new Object() 就是典型的强引用. 能让程序员通过强引用访问到的对象, 不会被 JVM 垃圾回收, 即使内存空间不够, JVM 也不会回收这些对象, 而是抛出内存溢出异常;
软引用: 软引用描述的是一些还有用, 但不是必须的对象. 被软引用所引用的对象, 也不会被垃圾回收, 直到 JVM 将要发生内存溢出异常时, 才会将这些对象列为回收对象, 进行回收. 在 JDK1.2 之后, 提供了 SoftReference 类实现软引用;
弱引用: 弱引用描述的是非必须的对象, 被弱引用所引用的对象, 只能生存到下一次垃圾回收前, 下一次垃圾回收来临, 此对象就会被回收. 在 JDK1.2 之后, 提供了 WeakReference 类实现弱引用 (也就是上面 Entry 继承的类);
虚引用: 这是最弱的一种引用关系, 一个对象是否有虚引用, 完全不会对其生存时间产生影响, 我们也不能通过一个虚引用访问对象, 使用虚引用的唯一目的就是, 能在这个对象被回收时, 受到一个系统的通知. JDK1.2 之后, 提供了 PhantomReference 实现虚引用;
介绍完各类引用的概念, 我们就可以来分析一下 Entry 为什么需要继承 WeakReference 类了. 从代码中, 我们可以看到, Entry 将 key 值, 也就是 ThreadLocal 的引用传入到了 WeakReference 的构造方法中, 也就是说在 ThreadLocalMap 中, key 的引用是弱引用. 这表明, 当没有其他强引用指向 key 时, 这个 key 将会在下一次垃圾回收时被 JVM 回收.
为什么需要这么做呢? 这么做的目的自然是为了有利于垃圾回收了. 如果了解过 JVM 的垃圾回收算法的应该知道, JVM 判断一个对象是否需要被回收, 判断的依据是这个对象还能否被我们所使用, 举个简单的例子:
- public static void main(String[] args) {
- Object o = new Object();
- o = null;
- }
上面的代码中, 我们创建了一个对象, 并使用强引用 o 指向它, 然后我们将 o 置为空, 这个时候刚刚创建的对象就丢失了, 因为我们无法通过任何引用找到这个对象, 从而使用它, 于是这个对象就需要被回收, 这种判断依据被称为可达性分析. 关于 JVM 的垃圾回收算法, 可以参考这篇博客: Java 中的垃圾回收算法详解.
好, 回归正题, 我们开始分析为什么 ThreadLocalMap 需要让 key 使用弱引用. 假设我们创建了一个 ThreadLocal, 使用完之后没有用了, 我们希望能够让它被 JVM 回收, 于是有了下面这个过程:
- // 创建 ThreadLocal 对象
- ThreadLocal tl = new ThreadLocal();
- // ..... 省略使用的过程...
- // 使用完成, 希望被 JVM 回收, 于是执行以下操作, 解除强引用
- tl = null;
我们在使用完 ThreadLocal 之后, 解除对它的强引用, 希望它被 JVM 回收. 但是 JVM 无法回收它, 因为我们虽然在此处释放了对它的强引用, 但是它还有其它强引用, 那就是 Thread 对象的 ThreadLocalMap 的 key. 我们之前反复说过, ThreadLocalMap 的 key 就是 ThreadLocal 对象的引用, 若这个引用是一个强引用, 那么在当前线程执行完毕, 被回收前, ThreadLocalMap 不会被回收, 而 ThreadLocalMap 不会被回收, 它的 key 引用的 ThreadLocal 也就不会回收, 这就是问题的所在. 而使用弱引用就可以保证, 在其他对 ThreadLocal 的强引用解除后, ThreadLocalMap 对它的引用不会影响 JVM 对它进行垃圾回收. 这就是使用弱引用的原因.
2.6 ThreadLocal 造成的内存溢出问题
上面描述了对 ThreadLocalMap 对 key 使用弱引用, 来避免 JVM 无法回收 ThreadLocal 的问题, 但是这里却还有另外一个问题. 我们看上面 Entry 的代码发现, key 值虽然使用的弱引用, 但是 value 使用的却是强引用. 这会造成一个什么问题? 这会造成 key 被 JVM 回收, 但是 value 却无法被收, key 对应的 ThreadLocal 被回收后, key 变为了 null, 但是 value 却还是原来的 value, 因为被 ThreadLocalMap 所引用, 将无法被 JVM 回收. 若 value 所占内存较大, 线程较多的情况下, 将持续占用大量内存, 甚至造成内存溢出. 我们通过一段代码演示这个问题:
- public class Main {
- public static void main(String[] args) {
- // 循环创建多个 TestClass
- for (int i = 0; i <100; i++) {
- // 创建 TestClass 对象
- TestClass t = new TestClass(i);
- // 调用反复
- t.printId();
- //************* 注意此处, 非常关键: 为了帮助回收, 将 t 置为 null
- t = null;
- }
- }
- static class TestClass {
- int id;
- // 每个 TestClass 对象对应一个很大的数组
- int[] arr = new int[100000000];
- // 每个 TestClass 对象对应一个 ThreadLocal 对象
- ThreadLocal<int[]> threadLocal = new ThreadLocal<>();
- TestClass(int id) {
- this.id = id;
- // threadLocal 存放的就是这个很大的数组
- threadLocal.set(arr);
- }
- public void printId() {
- System.out.println(id);
- }
- }
- }
上面的代码多次创建所占内存非常大的对象, 并在创建后, 立即解除对象的强引用, 让对象可以被 JVM 回收. 按道理来说, 上面的代码运行应该不会发生内存溢出, 因为我们虽然创建了多个大对象, 占用了大量空间, 但是这些对象立即就用不到了, 可以被垃圾回收, 而这个对象被垃圾回收后, 对象的 id, 数组, 和 threadLocal 成员都会被回收, 所以所占内存不会持续升高, 但是实际运行结果如下:
- 0
- 1
- 2
- Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
- at Main$TestClass.<init>(Main.java:23)
- at Main.main(Main.java:10)
可以看到, 很快就发生了内存溢出异常. 为什么呢? 需要注意到, 在 TestClass 的构造方法中, 我们将数组 arr 放入了 ThreadLocal 对象中, 也就是被放进了当前线程的 ThreadLocalMap 中, 作为 value 存在. 我们前面说过, ThreadLocalMap 的 value 是强引用, 这也就意味着虽然 ThreadLocal 可以被正常回收, 但是作为 value 的大数组无法被回收, 因为它仍然被 ThreadLocalMap 的强引用所指向. 于是 TestClass 对象的超大数组就一种在内存中, 占据大量空间, 我们连续创建了多个 TestClass, 内存很快就被占满了, 于是发生了内存溢出. 而 JDK 的开发人员自然发现了这个问题, 于是有了下面这个解决方案:
- public class Main {
- public static void main(String[] args) {
- for (int i = 0; i <100; i++) {
- TestClass t = new TestClass(i);
- t.printId();
- //********** 注意, 与上面的代码只有此处不同 ************
- // 此处调用了 ThreadLocal 对象的 remove 方法
- t.threadLocal.remove();
- t = null;
- }
- }
- static class TestClass {
- int id;
- int[] arr;
- ThreadLocal<int[]> threadLocal;
- TestClass(int id) {
- this.id = id;
- arr = new int[100000000];
- threadLocal = new ThreadLocal<>();
- threadLocal.set(arr);
- }
- public void printId() {
- System.out.println(id);
- }
- }
- }
上面的代码中, 我们在将 t 置为空时, 先调用了 ThreadLocal 对象的 remove 方法, 这样做了之后, 再看看运行结果:
- 0
- 1
- 2
- // .... 神略中间部分
- 98
- 99
做了上面的修改后, 没有再发生内存溢出异常, 程序正常执行完毕. 这是为什么呢? ThreadLocal 的 remove 方法究竟有什么作用. 其实 remove 方法的作用非常简单, 执行 remove 方法时, 会从当前线程的 ThreadLocalMap 中删除 key 为当前 ThreadLocal 的那一个记录, key 和 value 都会被置为 null, 这样一来, 就解除了 ThreadLocalMap 对 value 的强引用, 使得 value 可以正常地被 JVM 回收了. 所以, 今后如果我们确认不再使用的 ThreadLocal 对象, 一定要记得调用它的 remove 方法.
我们之前说过, 如果我们没有调用 remove 方法, 那就会导致 ThreadLocal 在使用完毕后, 被正常回收, 但是 ThreadLocalMap 中存放的 value 无法被回收, 此时将会在 ThreadLocalMap 中出现 key 为 null, 而 value 不为 null 的元素. 为了减少已经无用的对象依旧占用内存的现象, ThreadLocal 底层实现中, 在操作 ThreadLocalMap 的过程中, 线程若检测到 key 为 null 的元素, 会将此元素的 value 置为 null, 然后将这个元素从 ThreadLocalMap 中删除, 占用的内存就可以让 JVM 将其回收. 比如说在 getEntry 方法中, 或者是 Map 扩容的方法中等.
三, 总结
ThreadLocal 实现线程独立的方式是直接将值存放在 Thread 对象的 ThreadLocalMap 中, Map 的 key 就是 ThreadLocal 的引用, 且为了有助于 JVM 进行垃圾回收, key 使用的是弱引用. 在使用 ThreadLocal 后, 一定要记得调用 remove 方法, 有助于 JVM 对 value 的回收.
四, 参考
《深入理解 Java 虚拟机 (第二版)》
https://mp.weixin.qq.com/s/Y24LQwukYwXueTS6NG2kKA
来源: https://www.cnblogs.com/tuyang1129/p/12713815.html