1. 从一次项目经历说起
在上家公司做 spark 的任务调度系统时, 碰到过这么一个需求:
1. 任务由一个线程执行, 同时在执行过程中会创建多个线程执行子任务, 子线程在执行子任务时又会创建子线程执行子任务的子任务. 整个任务结构就像一棵高度为 3 的树.
2. 每个任务在执行过程中会生成一个任务 ID, 我需要把这个任务 ID 传给子线程执行的子任务, 子任务同时也会生成自己的任务 ID, 并把自己的任务 ID 向自己的子任务传递.
流程可由下图所示
解决方案有很多, 比如借助外部存储如数据库, 或者自己在内存中维护一个存储 ID 的数据结构. 考虑到系统健壮性和可维护性, 最后采用了 jdk 中的
InheritableThreadLocal
来实现这个需求.
来看下 InheritableThreadLocal 的结构
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
InheritableThreadLocal 继承自 ThreadLocal,ThreadLocal 可以说是一个存储线程私有变量的容器 (当然这个说法严格来说不准确, 后面我们就知道为什么), 而 InheritableThreadLocal 正如 Inheritable 所暗示的那样, 它是可继承的: 使用它可使子线程继承父线程的所有线程私有变量. 因此我写了个工具类, 底层使用 InheritableThreadLocal 来存储任务的 ID, 并且使该 ID 能够被子线程继承.
- public class InheritableThreadLocalUtils {
- private static final ThreadLocal<Integer> local = new InheritableThreadLocal<>();
- public static void set(Integer t) {
- local.set(t);
- }
- public static Integer get() {
- return local.get();
- }
- public static void remove() {
- local.remove();
- }
- }
可以通过这个工具类的 set 方法和 get 方法分别实现任务 ID 的存取. 然而在 Code Review 的時候, 有同事觉得我这代码写的有问题: 原因大概是 InheritableThreadLocal 在这里只有一个, 子线程的任务 ID 在存储的时候会相互覆盖掉. 真的会这样吗? 为此我们用代码测试下:
- public static void main(String[] args) {
- ExecutorService executorService = Executors.newCachedThreadPool();
- for(int i=0;i<10;i++){
- executorService.execute(new TaskThread(i));
- }
- }
- static class TaskThread implements Runnable{
- Integer taskId;
- public TaskThread(Integer taskId) {
- this.taskId = taskId;
- }
- @Override
- public void run() {
- InheritableThreadLocalUtils.set(taskId);
- ExecutorService executor = Executors.newSingleThreadExecutor();
- executor.execute(new Runnable() {
- @Override
- public void run() {
- System.out.println(InheritableThreadLocalUtils.get());
- }
- });
- }
- }
这段代码开启了 10 个线程标号从 0 到 9, 我们在每个线程中将对应的标号存储到 InheritableThreadLocal, 然后开启一个子线程, 在子线程中获取 InheritableThreadLocal 中的变量. 最后的结果如下
每个线程都准确的获取到了父线程对应的 ID, 可见并没有覆盖的问题. InheritableThreadLocal 确实是用来存储和获取线程私有变量的, 但是真实的变量并不是存储在这个 InheritableThreadLocal 对象中, 它只是为我们存取线程私有变量提供了入口而已. 因为 InheritableThreadLocal 只是在 ThreadLocal 的基础上提供了继承功能, 为了弄清这个问题我们研究下 ThreadLocal 的源码.
2. ThreadLocal 源码解析
ThreadLocal 主要方法有两个, 一个 set 用来存储线程私有变量, 一个 get 用来获取线程私有变量.
2.1 set 方法源码解析
- /**
- * Sets the current thread's copy of this thread-local variable
- * to the specified value. Most subclasses will have no need to
- * override this method, relying solely on the {@link #initialValue}
- * method to set the values of thread-locals.
- *
- * @param value the value to be stored in the current thread's copy of
- * this thread-local.
- */
- public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- }
Thread t = Thread.currentThread() 获取了当前线程实例 t, 继续跟进第二行的 getMap 方法,
- /**
- * Get the map associated with a ThreadLocal. Overridden in
- * InheritableThreadLocal.
- *
- * @param t the current thread
- * @return the map
- */
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
t 是线程实例, 而 threadLocals 明显是 t 的一个成员变量, 进入一探究竟
- /* ThreadLocal values pertaining to this thread. This map is maintained
- * by the ThreadLocal class. */
- ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap 是个什么结构?
- static class ThreadLocalMap {
- /**
- * The entries in this hash map extend WeakReference, using
- * its main ref field as the key (which is always a
- * ThreadLocal object). Note that null keys (i.e. entry.get()
- * == null) mean that the key is no longer referenced, so the
- * entry can be expunged from table. Such entries are referred to
- * as "stale entries" in the code that follows.
- */
- static class Entry extends WeakReference<ThreadLocal<?>> {
- /** The value associated with this ThreadLocal. */
- Object value;
- Entry(ThreadLocal<?> k, Object v) {
- super(k);
- value = v;
- }
- }
ThreadLocalMap 是类 Thread 中的一个静态内部类, 看起来像一个 HashMap, 但和 HashMap 又有些不一样 (关于它们的区别后面会讲), 那我们就把它当一个特殊的 HashMap 好了. 因此 set 方法中第二行代码
ThreadLocalMap map = getMap(t) 是通过线程实例 t 得到一个 ThreadLocalMap. 接下来的代码
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- /**
- * Create the map associated with a ThreadLocal. Overridden in
- * InheritableThreadLocal.
- *
- * @param t the current thread
- * @param firstValue value for the initial entry of the map
- */
- void createMap(Thread t, T firstValue) {
- t.threadLocals = new ThreadLocalMap(this, firstValue);
- }
如果这个 threadlocalmap 为 null, 先创建一个 threadlocalmap, 然后以当前 threadlocal 对象为 key, 以要存储的变量为值存储到 threadlocalmap 中.
2.2 get 方法源码解析
- /**
- * Returns the value in the current thread's copy of this
- * thread-local variable. If the variable has no value for the
- * current thread, it is first initialized to the value returned
- * by an invocation of the {@link #initialValue} method.
- *
- * @return the current thread's value of this thread-local
- */
- public T get() {
- Thread t = Thread.currentThread();
- 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;
- }
- }
- return setInitialValue();
- }
首先获取当前线程实例 t, 然后通过 getMap(t) 方法得到 threadlocalmap(ThreadLocalMap 是 Thread 的成员变量). 若这个 map 不为 null, 则以 threadlocal 为 key 获取线程私有变量, 否则执行 setInitialValue 方法. 看下这个方法的源码
- private T setInitialValue() {
- T value = initialValue();
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- return value;
- }
- protected T initialValue() {
- return null;
- }
首先获取 threadlocal 的初始化值, 默认为 null, 可以通过重写自定义该值; 如果 threadlocalmap 为 null, 先创建一个; 以当前 threadlocal 对象为 key, 以初始化值为 value 存入 map 中, 最后返回这个初始化值.
2.3 ThreadLocal 源码总结
总的来说, ThreadLocal 的源码并不复杂, 但是逻辑很绕. 现总结如下:
1.ThreadLocal 对象为每个线程存取私有的本地变量提供了入口, 变量实际存储在线程实例的内部一个叫 ThreadLocalMap 的数据结构中.
2.ThreadLocalMap 是一个类 HashMap 的数据结构, Key 为 ThreadLoca 对象 (其实是一个弱引用),Value 为要存储的变量值.
3. 使用 ThreadLocal 进行存取, 其实就是以 ThreadLocal 对象为隐含的 key 对各个线程私有的 Map 进行存取.
可以用下图的内存图像帮助理解和记忆
3. ThreadLocalMap 详解
先看源码
- static class ThreadLocalMap {
- /**
- * The entries in this hash map extend WeakReference, using
- * its main ref field as the key (which is always a
- * ThreadLocal object). Note that null keys (i.e. entry.get()
- * == null) mean that the key is no longer referenced, so the
- * entry can be expunged from table. Such entries are referred to
- * as "stale entries" in the code that follows.
- */
- static class Entry extends WeakReference<ThreadLocal<?>> {
- /** The value associated with this ThreadLocal. */
- Object value;
- Entry(ThreadLocal<?> k, Object v) {
- super(k);
- value = v;
- }
- }
3.1 ThreadLocalMap 的 key 为弱引用
ThreadLocalMap 的 key 并不是 ThreadLocal, 而是 WeakReference, 这是一个弱引用, 说它弱是因为如果一个对象只被弱引用引用到, 那么下次垃圾收集时就会被回收掉. 如果引用 ThreadLocal 对象的只有 ThreadLocalMap 的 key, 那么下次垃圾收集过后该 key 就会变为 null.
3.2 为何要用弱引用
减少了内存泄漏. 试想我曾今存储了一个 ThreadLocal 对象到 ThreadLocalMap 中, 但后来我不需要这个对象了, 只有 ThreadLocalMap 中的 key 还引用了该对象. 如果这是个强引用的话, 该对象将一直无法回收. 因为我已经失去了其他所有该对象的外部引用, 这个 ThreadLocal 对象将一直存在, 而我却无法访问也无法回收它, 导致内存泄漏. 又因为 ThreadLocalMap 的生命周期和线程实例的生命周期一致, 只要该线程一直不退出, 比如线程池中的线程, 那么这种内存泄漏问题将会不断积累, 直到导致系统奔溃. 而如果是弱引用的话, 当 ThreadLocal 失去了所有外部强引用的话, 下次垃圾收集该 ThreadLocal 对象将被回收, 对应的 ThreadLocalMap 中的 key 将为 null. 下次 get 和 set 方法被执行时将会对 key 为 null 的 Entry 进行清理. 有效的减少了内存泄漏的可能和影响.
3.3 如何真正避免内存泄漏
及时调用 ThreadLocal 的 remove 方法
及时销毁线程实例
4. 总结
ThreadLocal 为我们存取线程私有变量提供了入口, 变量实际存储在线程实例的 map 结构中; 使用它可以让每个线程拥有一份共享变量的拷贝, 以非同步的方式解决多线程对资源的争用
来源: https://www.cnblogs.com/takumicx/p/9320881.html