Java 并发编程 ③ - ThreadLocal 和 InheritableThreadLocal 详解

Richard_Yi 2020年03月21日 279次浏览

前言

往期文章:

继上一篇结尾讲的,这一篇文章主要是讲ThreadLocal 和 InheritableThreadLocal。主要内容有:

  • ThreadLocal 使用 和 实现原理

  • ThreadLocal 副作用

    • 脏数据
    • 内存泄漏的分析
  • InheritableThreadLocal 使用 和 实现原理

一、ThreadLocal

ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,即变量在线程间隔离而在方法或类间共享的场景。 确切的来说,ThreadLocal 并不是专门为了解决多线程共享变量产生的并发问题而出来的,而是给提供了一个新的思路,曲线救国。

通过实例代码来简单演示下ThreadLocal的使用。

public class ThreadLocalExample {

    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {

        ExecutorService service = Executors.newCachedThreadPool();

        service.execute(() -> {
            System.out.println(Thread.currentThread().getName() + " set 1");
            threadLocal.set(1);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 不会收到线程2的影响,因为ThreadLocal 线程本地存储
            System.out.println(Thread.currentThread().getName() + " get " + threadLocal.get());
            threadLocal.remove();
        });

        service.execute(() -> {
            System.out.println(Thread.currentThread().getName() + " set 2");
            threadLocal.set(2);
            threadLocal.remove();
        });

        ThreadPoolUtil.tryReleasePool(service);
    }
}

可以看到,线程1不会受到线程2的影响,因为ThreadLocal 创建的是线程私有的变量。

二、ThreadLocal 实现原理 ⭐

2.1 理清 ThreadLocal 中几个关键类之间的关系

我们先看下ThreadLocal 与 Thread 的类图,了解他们的主要方法和相互之间的关系。

图中几个类我们标注一下:

  • Thread
  • ThreadLocal
  • ThreadLocalMap
  • ThreadLocalMap.Entry

接下去,我们首先先开始了解这几个类的相互关系:

  1. Thread 类中有一个 threadLocals 成员变量(实际上还有一个inheritableThreadLocals,后面讲),它的类型是ThreadLocal 的内部静态类ThreadLocalMap

    public class Thread implements Runnable {
    
      	// ...... 省略
    
    	/* ThreadLocal values pertaining to this thread. This map is maintained
         * by the ThreadLocal class. */
        ThreadLocal.ThreadLocalMap threadLocals = null;
    
    
  2. ThreadLocalMap 是一个定制化的Hashmap,为什么是个HashMap?很好理解,每个线程可以关联多个ThreadLocal变量。

        /**
         * ThreadLocalMap is a customized hash map suitable only for
         * maintaining thread local values. No operations are exported
         * outside of the ThreadLocal class. The class is package private to
         * allow declaration of fields in class Thread.  To help deal with
         * very large and long-lived usages, the hash table entries use
         * WeakReferences for keys. However, since reference queues are not
         * used, stale entries are guaranteed to be removed only when
         * the table starts running out of space.
         */
        static class ThreadLocalMap {
            // ...
        }
    
  3. ThreadLocalMap 初始化时会创建一个大小为16的Entry 数组,Entry 对象也是用来保存 key- value 键值对(这个Key固定是ThreadLocal 类型)。值得注意的是,这个Entry 继承了 WeakReference(这个设计是为了防止内存泄漏,后面会讲)

            static class Entry extends WeakReference<ThreadLocal<?>> {
           /** The value associated with this ThreadLocal. */
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    

2.2 ThreadLocal的set、get及remove方法的源码

a. void set(T value)

    public void set(T value) {
        // ① 获取当前线程
        Thread t = Thread.currentThread();
        // ② 去查找对应线程的ThreadLocalMap变量
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
			// ③ 第一次调用就创建当前线程的对应的ThreadLocalMap
            // 并且会将值保存进去,key是当前的threadLocal,value就是传进来的值
            createMap(t, value);
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

b. T get()

    public T get() {
        // ① 获取当前线程
        Thread t = Thread.currentThread();
        // ② 去查找对应线程的ThreadLocalMap变量
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // ③ 不为null,返回当前threadLocal 对应的value值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // ④ 当前线程的threadLocalMap为空,初始化
        return setInitialValue();
    }

    private T setInitialValue() {
        // ⑤ 初始化的值为null
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            // 初始化当前线程的threadLocalMap
            createMap(t, value);
        return value;
    }

    protected T initialValue() {
        return null;
    }

c. void remove()

如果当前线程的threadLocals变量不为空,则删除当前线程中指定ThreadLocal实例对应的本地变量。

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

从源码中可以看出来,自始至终,这些本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量,那个线程私有的threadLocalMap 里面

ThreadLocal就是一个工具壳和一个key,它通过set方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用。

讲到这里,实现原理就算讲完了,实际上ThreadLocal 的源码算是非常简单易懂。关于ThreadLocal 真正的重点和难点,是我们后面的内容。

三、ThreadLocal 副作用

ThreadLocal 是为了线程能够安全的共享/传递某个变量设计的,但是有一定的副作用。

ThreadLocal 的主要问题是会产生脏数据内存泄露

先说一个结论,这两个问题通常是在线程池的线程中使用 ThreadLocal 引发的,因为线程池有线程复用内存常驻两个特点。

3.1 脏数据

脏数据应该是大家比较好理解的,所以这里呢,先拿出来讲。线程复用会产生脏数据。由于线程池会重用 Thread 对象 ,那么与 Thread 绑定的类的静态属性 ThreadLocal 变量也会被重用。如果在实现的线程 run() 方法体中不显式地调用 remove() 清理与线程相关的 ThreadLocal 信息,那么倘若下一个线程不调用 set() 设置初始值,就可能 get() 到重用的线程信息,包括 ThreadLocal 所关联的线程对象的 value 值。

为了便于理解,这里提供一个demo:

public class ThreadLocalDirtyDataDemo {

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

    public static void main(String[] args) {

        ExecutorService pool = Executors.newFixedThreadPool(1);

        for (int i = 0; i < 2; i++) {
            MyThread thread = new MyThread();
            pool.execute(thread);
        }
        ThreadPoolUtil.tryReleasePool(pool);
    }

    private static class MyThread extends Thread {
        private static boolean flag = true;

        @Override
        public void run() {
            if (flag) {
                // 第一个线程set之后,并没有进行remove
                // 而第二个线程由于某种原因(这里是flag=false) 没有进行set操作
                String sessionInfo = this.getName();
                threadLocal.set(sessionInfo);
                flag = false;
            }
            System.out.println(this.getName() + " 线程 是 " + threadLocal.get());
            // 线程使用完threadLocal,要及时remove,这里是为了演示错误情况
        }
    }
}

执行结果:

Thread-0 线程 是 Thread-0
Thread-1 线程 是 Thread-0

3.2 内存泄露 ⭐

在讲这个之前,有必要看一张图,从栈与堆的角度看看ThreadLocal 使用过程当中几个类的引用关系。

看到红色的虚线箭头没?这个就是理解ThreadLocal的一个重点和难点。

我们再看一遍Entry的源码:

          static class Entry extends WeakReference<ThreadLocal<?>> {
              /** The value associated with this ThreadLocal. */
              Object value;
  
              Entry(ThreadLocal<?> k, Object v) {
                  super(k);
                  value = v;
              }
          }

ThreadLocalMap 的每个 Entry 都是一个对的弱引用 - WeakReference<ThreadLocal<?>>,这一点从super(k)可看出。另外,每个 Entry都包含了一个对 的强引用。

在前面的叙述中,我有提到Entry extends WeakReference<ThreadLocal<?>> 是为了防止内存泄露。实际上,这里说的防止内存泄露是针对ThreadLocal对象的

怎么说呢?继续往下看。

如果你有学习过Java 中的引用的话,这个WeakReference应该不会陌生,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收只被弱引用关联的对象。

更详细的相关内容可以阅读笔者的这篇文章 【万字精美图文带你掌握JVM垃圾回收#Java 中的引用】

通过这种设计,即使线程正在执行中, 只要 ThreadLocal 对象引用被置成 null,Entry 的 Key 就会自动在下一次 YGC 时被垃圾回收(因为只剩下ThreadLocalMap 对其的弱引用,没有强引用了)

如果这里Entry 的key 值是对 ThreadLocal 对象的强引用的话,那么即使ThreadLocal的对象引用被声明成null 时,这些 ThreadLocal 不能被回收,因为还有来自 ThreadLocalMap 的强引用,这样子就会造成内存泄漏

这类key被回收( key == null)的Entry 在 ThreadLocalMap 源码中被称为 stale entry (翻译过来就是 “过时的条目”),会在下一次执行 ThreadLocalMap 的 getEntry 和 set 方法中,将 这些 stale entry 的value 置为 null,使得原来value 指向的变量可以被垃圾回收

“会在下一次执行 ThreadLocalMap 的 getEntry 和 set 方法中,将 这些 stale entry 的value 置为 null,使得 原来value 指向的变量可以被垃圾回收”这一部分描述,可以查阅 ThreadLocalMap#expungeStaleEntry()方法源码及调用这个方法的地方。

这样子来看,ThreadLocalMap 是通过这种设计,解决了 ThreadLocal 对象可能会存在的内存泄漏的问题并且对应的value 也会因为上述的 stale entry 机制被垃圾回收


但是我们为什么还会说使用ThreadLocal 可能存在内存泄露问题呢,在这里呢,指的是还存在那个Value(图中的紫色块)实例无法被回收的情况

请注意哦,上述机制的前提是ThreadLocal 的引用被置为null,才会触发弱引用机制,继而回收Entry 的 Value对象实例。我们来看下ThreadLocal 源码中的注释

instances are typically private static fields in classes

ThreadLocal 对象通常作为私有静态变量使用

-- 如果说一个 ThreadLocal 是非静态的,属于某个线程实例类,那就失去了线程内共享的本质属性。

作为静态变量使用的话, 那么其生命周期至少不会随着线程结束而结束。也就是说,绝大多数的静态threadLocal对象都不会被置为null。这样子的话,通过 stale entry 这种机制来清除Value 对象实例这条路是走不通的。必须要手动remove() 才能保证。

这里还是用上面的例子来做示例。

public class ThreadLocalDirtyDataDemo {

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

    public static void main(String[] args) {

        ExecutorService pool = Executors.newFixedThreadPool(1);

        for (int i = 0; i < 2; i++) {
            MyThread thread = new MyThread();
            pool.execute(thread);
        }
        ThreadPoolUtil.tryReleasePool(pool);
    }

    private static class MyThread extends Thread {
        private static boolean flag = true;

        @Override
        public void run() {
            if (flag) {
                // 第一个线程set之后,并没有进行remove
                // 而第二个线程由于某种原因(这里是flag=false) 没有进行set操作
                String sessionInfo = this.getName();
                threadLocal.set(sessionInfo);
                flag = false;
            }
            System.out.println(this.getName() + " 线程 是 " + threadLocal.get());
            // 线程使用完threadLocal,要及时remove,这里是为了演示错误情况
        }
    }
}

在这个例子当中,如果不进行 remove() 操作, 那么这个线程执行完成后,通过 ThreadLocal 对象持有的 String 对象是不会被释放的。

为什么说只有线程复用的时候,会出现这个问题呢?当然啦,因为这些本地变量都是存储在线程的内部变量中的,当线程销毁时,threadLocalMap的对象引用会被置为null,value实例对象 随着线程的销毁,在内存中成为了不可达对象,然后被垃圾回收。

    // Thread#exit()
	private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        /* Speed the release of some of these resources */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }

总结

总结一下

  • WeakReference 的引入,是为了将ThreadLocal 对象与ThreadLocalMap 设计成一种弱引用的关系,来避免ThreadLocal 实例对象不能被回收而存在的内存泄露问题,当threadLocal 对象被回收时,会有清理 stale entry 机制,回收其对应的Value实例对象。
  • 我们常说的内存泄露问题,针对的是threadLocal对应的Value对象实例。在线程对象被重用且threadLocal为静态变量时,如果没有手动remove(),就可能会造成内存泄露的情况。
  • 上述两种内存泄露的情况只有在线程复用的情况下才会出现,因为在线程销毁时threadLocalMap的对象引用会被置为null。
  • 解决副作用的方法很简单,就是每次用完ThreadLocal,都要及时调用 remove() 方法去清理。

四、InheritableThreadLocal

在一些场景中,子线程需要可以获取父线程的本地变量,比如用一个统一的ID来追踪记录调用链路。但是ThreadLocal 是不支持继承性的,同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到对应的对象的。

为了解决这个问题,InheritableThreadLocal 也就应运而生。

4.1 使用

public class InheritableThreadLocalDemo {

    private static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        // 主线程
        threadLocal.set("hello world");
        // 启动子线程
        Thread thread = new Thread(() -> {
            // 子线程输出父线程的threadLocal 变量值
            System.out.println("子线程: " + threadLocal.get());
        });

        thread.start();

        System.out.println("main: " +threadLocal.get());

    }
}

输出:

main: hello world
子线程: hello world

4.2 原理

要了解原理,我们先来看一下 InheritableThreadLocal 的源码。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    // ①
    protected T childValue(T parentValue) {
        return parentValue;
    }

	// ②
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    // ③
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}
public class Thread implements Runnable {

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

可以看到,InheritableThreadLocal 继承了ThreadLocal,并且重写了三个方法,看来实现的门道就在这三个方法里面。

先看代码③,InheritableThreadLocal 重写了createMap方法,那么现在当第一次调用set方法时,创建的是当前线程的inheritableThreadLocals 变量的实例而不再是threadLocals。由代码②可知,当调用get方法获取当前线程内部的map变量时,获取的是inheritableThreadLocals而不再是threadLocals。

可以这么说,在InheritableThreadLocal的世界里,变量inheritableThreadLocals替代了threadLocals。

代码②③都讲了,再来看看代码①,以及如何让子线程可以访问父线程的本地变量。

这要从创建Thread的代码说起,打开Thread类的默认构造函数,代码如下。

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
		
        // ... 省略无关部分
        // 获取父线程 - 当前线程
        Thread parent = currentThread();
		
        // ... 省略无关部分
        // 如果父线程的inheritThreadLocals不为null 且 inheritThreadLocals=true
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            // 设置子线程中的inheritableThreadLocals变量
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
		// ... 省略无关部分
    }

    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

再来看看里面是如何执行createInheritedMap 的。

        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        // 这里调用了重写的代码① childValue
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

在该构造函数内部把父线程的inheritableThreadLocals成员变量的值复制到新的ThreadLocalMap 对象中。

小结

本章讲了ThreadLocal 和 InheritableThreadLocal 的相关知识点。

ThreadLocal 实现线程内部变量共享,InheritableThreadLocal 实现了父线程与子线程的变量继承。但是还有一种场景,InheritableThreadLocal 无法解决,也就是在使用线程池等会池化复用线程的执行组件情况下,异步执行执行任务,需要传递上下文的情况

针对上述情况,阿里开源了一个TTL,即Transmittable ThreadLocal来解决这个问题,有兴趣的朋友们可以去看看。

之后有时间的话我会单独写一篇文章介绍一下。

如果本文有帮助到你,希望能点个赞,这是对我的最大动力🤝🤝🤗🤗。

参考

  • 《Java 并发编程之美》
  • 《码出高效》