ThreadLocal源码分析总结

ThreadLocal源码分析总结

@author:Jingdai
@date:2021.07.25

上篇分析了ThreadLocalMap的源码,这篇继续分析ThreadLocal的源码。本文将会分析ThreadLocal的源码、内存泄露问题及 ThreadLocal 和线程池结合使用的问题。

简介

ThreadLocal 用来创建线程局部变量,使每个线程有自己独立的变量副本,不会有线程安全问题。ThreadLocal 通常定义为private static 的属性,用以存储与线程相关联的状态(如user ID、事务ID)。看Java-8 documentation 中给的示例。

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadId {
    // Atomic integer containing the next thread ID to be assigned
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // Thread local variable containing each thread's ID
    private static final ThreadLocal<Integer> threadId =
        new ThreadLocal<Integer>() {
            @Override protected Integer initialValue() {
                return nextId.getAndIncrement();
        }
    };

    // Returns the current thread's unique ID, assigning it if necessary
    public static int get() {
        return threadId.get();
    }
}

这个类用来生成每个线程唯一的ID,线程的ID在第一次调用 get 方法时被分配,并且在之后保持不变。

只要线程活着并且可以访问 ThreadLocal 实例,那么这个线程就有一条到线程局部变量的引用链。当线程运行结束后,它的所有线程局部变量都将会被垃圾回收(除非有其他引用指向它们)。下面具体进行介绍。

ThreadLocal 和相关类的关系

在开始分析源码之前,先看看 ThreadLocal 和 Thread 及 ThreadLocalMap 之间的关系。

Thread类

public
class Thread implements Runnable {
    
    // xx
        
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    // xx
}

Thread类内部有一个 ThreadLocal.ThreadLocalMap 类型的 threadLocals 变量,这是一个map,就是用这个 map 来存储相应的线程局部变量。每个线程都对应一个的 Thread 实例,每个 Thread 实例都有自己的 threadLocals 属性,互不干扰,这就是 ThreadLocal 能提供线程局部变量的原因。当线程结束时,对应的 Thread 实例将会被回收,所以这个 threadLocals 也就会被回收,即所有相关的线程局部变量都会被回收。同时,注意到这个变量的权限修饰符是默认的权限修饰符,即包可见,而 Thread 类在 java.lang 包下,所以我们自己写的代码是无法直接访问到这个 threadLocals 变量的。

ThreadLocalMap 类

关于 ThreadLocalMap,有兴趣了解其详细实现的同学可以看我上一篇关于 ThreadLocalMap 的源码分析,这里仅仅分析它和 ThreadLocal 的关系。

public class ThreadLocal<T> {
    
    // xx
    
    static class ThreadLocalMap {
        // xx
    }
    
    // xx
}

从源码可以看出,ThreadLocalMap 是 ThreadLocal 的 一个内部类。这个类访问修饰符是默认,即包可见,ThreadLocal 也是 java.lang 包下的,这就使得 Thread 类可以声明一个 ThreadLocalMap 类型的属性。ThreadLocalMap 类似于 HashMap,如果不想了解其具体源码的可以暂且当它是一个HashMap。map 里 Entry的 key 是指向 ThreadLocal 变量的弱引用,而值是通过 ThreadLocal 存入的值。 ThreadLocal 作为键,将对应的键值对存入到了当前线程的 Thread 实例的 threadLocals 属性中。由此,可以将 ThreadLocal 当做是当前线程的 threadLocals 变量和用户代码的中介,因为 threadLocals 属性只能在 java.lang 包下直接访问,用户代码通过ThreadLocal 间接的向当前线程的 threadLocals 中存取数据。

ThreadLocal 源码分析

set 方法

public void set(T value) {
    // 获得当前线程 t
    Thread t = Thread.currentThread();
    // 获得 t 的 ThreadLocalMap 对象
    // 即 threadLocals
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 将<this, value> 键值对
        // 放入 threadLocals 对象中
        map.set(this, value);
    else
        // 创建map,并将<this, value> 键值对
        // 放入 threadLocals 对象中
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
    // 给 threadLocals 赋初始
    // 并插入<this, firstValue> 键值对
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

由源码可见,ThreadLocal 的 set 方法首先会去获取当前线程 thread 的 threadLocals属性,如果 threadLocals 为空,就初始化并将 <this, value> 键值对插入;如果不为空,则直接将 <this, value> 键值对插入。而 ThreadLocalMap 插入的细节这里就不细讲了,我上一篇文章中有详细介绍,有兴趣的可以去看看。

get 方法

public T get() {
    // 获得当前线程对象
    Thread t = Thread.currentThread();
    // 获得 t.threadLocals
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 如果 map == null || e == null
    // 返回 setInitialValue 的返回值
    return setInitialValue();
}
private T setInitialValue() {
    // 获取初始值
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 将初始值 value 插入 map中
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
// 返回此 ThreadLocal 对象关于当前线程对应的初始值
// 当用户想要 ThreadLocal 想要为线程创建初始值时,
// 需要覆盖这个方法,通常使用匿名内部类,如最上面
// thread Id 的例子。
protected T initialValue() {
    return null;
}

get 方法和 set 方法一样,还是先取得当前线程的 threadLocals,如果 threadLocals 不为 null 并且从中得到了对应的值,则直接返回得到的值;否则调用 setInitialValue,将初始值插入到 map 中并返回初始值。

remove 方法

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

删除当前线程 threadLocals 中这个 ThreadLocal 对象对应的键值对。

源码这就看完了,相比于其他的JDK源码,这个源码确实算是非常容易理解的了,当然,想详细了解ThreadLocalMap 的set、get等过程,可以看我上一篇的ThreadLocalMap的源码分析。

内存泄露分析

ThreadLocal 存在内存泄露的可能性,分析一下原因。在ThreadLocalMap 的 Entry 中,指向ThreadLocal的弱引用作为 key,则插入元素后某键值对的内存图如下,虚线表示弱引用。

在这里插入图片描述

当图中 ref 不再使用时,如将它置为null或对应的栈帧退出,则图中 k 的强引用就没了,只剩一个弱引用指向它,当发生GC时,k 就会被回收,但是 v 和 entry 不会被回收,我们也没有办法访问到这个 entry 和 v,因为前面讲过,我们需要 ref 这个中介才能去对 threadLocals 进行操作。最终的结果就是 entry 和 v 的内存泄露。

有些人认为内存泄露的原因是这个弱引用,其实根本不是,假设这里是强引用,当 ref 不再使用时,k 、v 及 entry 都不会被回收,比使用弱引用还多了一个 k 的内存泄露。那内存泄露的真正原因是什么呢?就是因为我们在不使用这个 entry 时没有将它置 null,那么这个 entry 就和当前线程的生命周期一致,只要线程不结束,它占用的内存就不会释放。同时,如果使用线程池,那可能会发生更加严重的问题,第一个任务用完线程没有清空 threadLocals 就将它放回线程池,导致下一个任务使用的线程拿到了上一个任务的 threadLocals,这就可能会发生业务相关的问题。ps,最后一节会举一个栗子。

那应该如何解决这个问题呢?在线程结束时,这个线程对应的 threadLocals 将会被回收,就不存在内存泄露。但是线程的生命周期不好控制,同时如前所述使用线程池时将会使情况更加复杂,所以不能依赖于线程的结束。正确的做法是使用完这个 ThreadLocal 时,记得调用它的 remove 方法,将这个 entry 置空就行。

最后,分析一下为什么源码中要使用弱引用,如果你看过 ThreadLocalMap 的源码就会很好理解了。源码中在 get、set、rehash 等方法中都会对看见的 key 为 null 的entry(源码中称为 stale entry)进行置空,但是这并不是对 table 上的所有的 stale entry进行置空,大多数情况下仅仅是处理临近的几个 stale entry,有兴趣的可以去查阅源码。所以,源码中使用弱引用相当于考虑了部分用户的不当使用,使得在下次操作时可以删除一部分不使用的 entry,减轻内存泄露的影响。当然,还是必须要记得不使用时要调用 remove 方法,弱引用仅能解决部分问题,不能完全依赖它。

线程池问题示例

创建一个只有两个核心线程数的线程池,向里面依次加入 4 个任务,仅仅在第一个任务中设置 ThreadLocal 的值。示例代码如下。


import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadPoolExecutor;

public class TestThreadLocal {

    private static ThreadLocal<String> nameThreadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        
        ExecutorService executorPool = 
            new ThreadPoolExecutor(2, 2, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
        
        for (int i = 0; i < 4; i++) {
            int temp = i;
            executorPool.execute(() -> {
                // 加锁仅仅是为了让结果更清晰,使每个线程的输出
                // 依次打印,不会线程间交替打印,看起来很乱
                synchronized (TestThreadLocal.class) {
                    // 仅仅向第一个线程中设置了ThreadLocal的值
                    if (temp == 0) {
                        nameThreadLocal.set("name");
                    }
                    System.out.println(Thread.currentThread().getName());
                    System.out.println("get:" + nameThreadLocal.get());
                    System.out.println("--------");
                } 
            });
        }
    }

}

如果对ThreadLocal不熟悉的话,可能就会认为只有第 1 个运行的线程打印的 get 会有值,其余都会是 null,但是结果不是这样的,结果如下。

pool-1-thread-1
get:name
--------
pool-1-thread-2
get:null
--------
pool-1-thread-1
get:name
--------
pool-1-thread-2
get:null
--------

分析一下,原因就是线程池中的线程使用完并不会结束,而是会交还给线程池。示例中有两个核心线程,第一个任务向第一个核心线程的 threadLocals 中插入了 name,做完任务后并没有清除,第三个任务执行时,又分配到了第一个核心线程,这个线程里面的 threadLocals 中有第一个任务插入的 name,故 get 还是可以取得第一个任务插入的 name,这就产生了问题。解决方法也很简单,如前所述,在任务执行结束时调用 ThreadLocal 的 remove 方法删除。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值