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

栏目: IT技术 · 发布时间: 4年前

内容简介:往期文章:继上一篇结尾讲的,这一篇文章主要是讲ThreadLocal 和 InheritableThreadLocal。主要内容有:ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,即变量在线程间隔离而在方法或类间共享的场景。确切的来说,ThreadLocal 并不是专门为了解决多线程共享变量产生的并发问题而出来的,而是给提供了一个新的思路,曲线救国。

前言

往期文章:

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

  • ThreadLocal 使用 和 实现原理

  • ThreadLocal 副作用

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

一、ThreadLocal

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

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

通过实例代码来简单演示下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 实现原理 :star:

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

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

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

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

  • 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 内存泄露 :star:

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

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

看到红色的虚线箭头没?这个就是理解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 并发编程之美》
  • 《码出高效》

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

三位一体

三位一体

[美]迈克尔·马隆 / 黄亚昌 / 浙江人民出版社 / 2015-4 / 98.90

[内容简介] ●本书讲述了罗伯特•诺伊斯、戈登•摩尔和安德鲁•格鲁夫如何缔造了世界上最重要公司的故事。公司的“外交家”诺伊斯被视为圣父、“思想家”摩尔被视为圣灵、“行动家”格鲁夫被视为圣子,这个三位一体的组合创下了企业管理中的奇迹,开创了一个价值万亿美元的产业,将一家初创企业打造成为千亿美元量级的巨型公司。 ●本书作者迈克尔•马隆在接触空前数量的企业档案的基础上,揭示了英特尔公司无处不......一起来看看 《三位一体》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具