ThreadLocal 续篇 ----- 父子线程如何共享数据

本贴最后更新于 1580 天前,其中的信息可能已经斗转星移

ThreadLocal 和 InheritableThreadLocal

父子线程共享数据可以通过使用 InheritableThreadLocal 来达成目的,InheritableThreadLocal 类继承于 ThreadLocal

ThreadLocal

主要成员变量

ThreadLocal 最主要的成员变量是 ThreadLocalMap,该对象用于存储我们需要保存的数据,ThreadLocalMap 使用 Entry 数组存储数据
image.png

Entry 继承自 WeakReference,使用 ThreadLocal 实例作为键,因此如果 Entry 的键没有被强引用持有那么当下次 GC 时就会被回收掉,该 Entry 实例的键就为 null 了,但是 value 依然存在
image.png

  • 强引用 StrongReference
    如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
  • 软引用 SoftReference
    如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
  • 弱引用 WeakReference
    弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
  • 虚引用 PhantomReference
    无法通过虚引用获取与之关联的对象实例,且当对象仅被虚引用引用时,在任何发生 GC 的时候,其均可被回收

主要方法

ThreadLocal 主要的数据结构就是 ThreadLocalMap 以及 Entry,接着我们看一下最主要的方法

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    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();
    }

    ThreadLocalMap getMap(Thread t) {
        // ThreadLocalMap在当前线程被所有ThreadLocal共享
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        // 初始化map,构建table与Enrty
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

ThreadLocal 在进行 set 时,把自己作为键。在 get 时首先获取当前 Thread,然后通过它获取到 ThreadLocalMap 实例,最后通过 ThreadLocalMap 实例获取到 value

threadLocalHashCode 和线性探测

因为 ThreadLocalMap 使用了 Entry 数组,所以我们可以知道这块设计类似于 HashMap 的 hash 桶,在存取数据时肯定要先确定 hash 桶的位置,这就要使用 hashCode 来定位了

image.png getEntry 是使用的 threadLocalHashCode 定位,我们来看一看这个变量

image.png threadLocalHashCode 的生成还依赖 HASH_INCREMENT,这是一个写死的数,十进制为 1640531527,每个 ThreadLocal 实例的 hashcode 都会根据这个值递增,并且这个递增是全局的,在整个 jvm 中不会存在重复的两个 threadLocalHashCode

HASH_INCREMENT 是一个很特殊的值,它能够让哈希码均匀的分布在 2 的 N 次方的数组里,也就是让 threadLocalHashCode & (table.length - 1) 的值非常均匀,也正是因为这个原因所以 ThreadLocalMap 没有采用 HashMap 的拉链法来解决 hash 冲突,而是使用线性探测

自动清除

之前在分析数据结构时发现 Entry 的 key 是一个弱引用,所以就可能会存在 key 被 GC 掉了但是 value 还存在。那么肯定有一个机制用于清除这种 Entry 实例,具体的方法就是 expungeStaleEntry

        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // 1.直接清除指定位置的Entry对象
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // 2.遍历Entry数组直到某个Entry对象的value不为null,然后把这期间遇到的所有key为null的都删除掉
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

注释中写的很清楚,主要有两个删除动作:

  • 1.直接清除指定位置的 Entry 对象
  • 2.遍历 Entry 数组直到某个 Entry 对象的 value 不为 null,然后把这期间遇到的所有 key 为 null 的都删除掉

主要是以下几个方法在调用 expungeStaleEntry(...) image.png

随便举一个调用链例子:ThreadLocal 的 get 方法 -> ThreadLocalMap 的 getEntry 方法 -> getEntryAfterMiss 方法 -> expungeStaleEntry 方法

所以不难得知在调用 ThreadLocal 进行 get 或者 remove 时都会把 key 为 null 的 Entry 对象给删除掉

InheritableThreadLocal

方法

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    public InheritableThreadLocal() {
    }

    protected T childValue(T var1) {
        return var1;
    }

    ThreadLocalMap getMap(Thread var1) {
        return var1.inheritableThreadLocals;
    }

    void createMap(Thread var1, T var2) {
        var1.inheritableThreadLocals = new ThreadLocalMap(this, var2);
    }
}

该类很简单,就重写了 ThreadLocal 的几个方法,在原来的 ThreadLocal 中 getMap 是调用 Thread 实例获取 threadLocals 对象,现在则是获取 ThreadinheritableThreadLocals 对象

image.png

image.png

同理在创建时,ThreadLocal 是将实例化的 ThreadLocalMap 赋给 threadLocals 变量,InheritableThreadLocal 则是将实例化的 ThreadLocalMap 赋给 inheritableThreadLocals 变量

父子线程共享数据

如果父线程存在 inheritableThreadLocals 变量(也就是 Thread 的实例中),那么在创建子线程时就会把 父线程的 inheritableThreadLocals 引用过来,然后就可以通过这个变量进行共享数据了,调用链如下

Thread 类:

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        .....
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        ....
    }

ThreadLocal 类:

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

最后关键的地方在于 ThreadLocalMap 的初始化,如下
image.png
代码很简单直观,就是通过父线程的 ThreadLocalMap 实例拿到 Entry 数组,然后浅拷贝一份,这样子线程就拥有了父线程通过 InheritableThreadLocals 设置的数据

这个 private ThreadLocalMap(ThreadLocalMap parentMap) 方法只有在父线程使用了 InheritableThreadLocals 才会用到

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3187 引用 • 8213 回帖
  • 代码
    467 引用 • 631 回帖 • 9 关注
1 操作
614756773 在 2020-07-26 15:56:01 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...