关于java:源码篇ThreadLocal的奇思妙想万字图文

61次阅读

共计 23086 个字符,预计需要花费 58 分钟才能阅读完成。

前言

ThreadLocal 的文章在网上也有不少,然而看了一些后,了解起来总感觉有绕,而且 看了 ThreadLocal 的源码,无论是线程隔离、类环形数组、弱援用构造等等,切实是太有意思了!我必须也要让大家全面感触下其中所蕴含的那些奇思妙想! 所以这里我想写一篇超几儿通俗易懂解析 ThreadLocal 的文章,相干流程会应用大量图示解析,以证实:我是干货,不是水比!

ThreadLocal 这个类加上宏大的正文,总共也才七百多行,而且你把这个类的代码拷贝进去,你会发现,它简直没有报错!耦合度极低!(惟一的报错是因为 ThreadLocal 类援用了 Thread 类里的一个包内可见变量,所以把代码复制进去,这个变量拜访就报错了,仅仅只有此处报错!)

ThreadLocal 的线程数据隔离,替换算法,擦除算法,都是有必要去理解理解,仅仅大量的代码,却能实现如此精妙的性能,让咱们来领会下 Josh Bloch 和 Doug Lea 俩位大神,鬼斧神工之作吧!

一些阐明

这篇文章画了不少图,大略画了十八张图,对于替换算法和擦除算法,这俩个办法所做的事件,如果不画图,光用文字描述的话,非常的形象且很难形容分明;心愿这些流程图,能让大家更能领会这些精炼代码的魅力!

应用

哔哔原理之前,必须要先来看下应用

  • 应用起来出奇的简略,仅仅应用 set()get()办法即可
public class Main {public static void main(String[] args) {ThreadLocal<String> threadLocalOne = new ThreadLocal<>();
        ThreadLocal<String> threadLocalTwo = new ThreadLocal<>();

        new Thread(new Runnable() {
            @Override
            public void run() {threadLocalOne.set("线程一的数据 --- threadLocalOne");
                threadLocalTwo.set("线程一的数据 --- threadLocalTwo");
                System.out.println(threadLocalOne.get());
                System.out.println(threadLocalTwo.get());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {System.out.println(threadLocalOne.get());
                System.out.println(threadLocalTwo.get());
                threadLocalOne.set("线程二的数据 --- threadLocalOne");
                threadLocalTwo.set("线程二的数据 --- threadLocalTwo");
                System.out.println(threadLocalOne.get());
                System.out.println(threadLocalTwo.get());
            }
        }).start();}
}
  • 打印后果

    • 一般来说,咱们在主存(或称工作线程)创立一个变量;在子线程中批改了该变量数据,子线程完结的时候,会将批改的数据同步到主存的该变量上
    • 然而,在此处,能够发现,俩个线程都应用同一个变量,然而在线程一外面设置的数据,齐全没影响到线程二
    • cool!简略易用,还实现了数据隔离与不同的线程
线程一的数据 --- threadLocalOne
线程一的数据 --- threadLocalTwo
null
null
线程二的数据 --- threadLocalOne
线程二的数据 --- threadLocalTwo

前置常识

在解释 ThreadLocal 整体逻辑前,须要先理解几个前置常识

上面这些前置常识,是在说 set 和 get 前,必须要先理解的知识点,理解上面这些知识点,能力更好去理解整个存取流程

线程隔离

在下面的 ThreadLocal 的应用中,咱们发现一个很乏味的事件,ThreadLocal 在不同的线程,如同可能存储不同的数据:就如同 ThreadLocal 自身具备存储性能,到了不同线程,可能生成不同的 '正本' 存储数据一样

实际上,ThreadLocal 到底是怎么做到的呢?

  • 来看下 set()办法,看看到底怎么存数据的:此处波及到 ThreadLocalMap 类型,暂且把他当成 Map,具体的前面栏目剖析

    • 其实这中央做了一个很有意思的操作:线程数据隔离的操作,是 Thread 类和 ThreadLocal 类相互配合做到的
    • 在上面的代码中能够看进去,在塞数据的时候,会获取执行该操作的以后线程
    • 拿到以后线程,取到 threadLocals 变量,而后好像以以后实例为 key,数据 value 的模式往这个 map 外面塞值(有区别,set 栏目再具体说)
    • 所以 应用 ThreadLocal 在不同的线程中进行写操作,实际上数据都是绑定在以后线程的实例上,ThreadLocal 只负责读写操作,并不负责保留数据,这就解释了,为什么 ThreadLocal 的 set 数据,只在操作的线程中有用
    • 大家有没有感觉这种思路有些奇妙!
// 存数据
public void set(T value) {Thread t = Thread.currentThread();
    ThreadLocal.ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

// 获取以后 Thread 的 threadLocals 变量
ThreadLocal.ThreadLocalMap getMap(Thread t) {return t.threadLocals;}

//Thread 类
public class Thread implements Runnable {
    ...
        
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    ...
}
  • 来看下图示

    • 图上只花了了一个 ThreadLocal,想多花几个,而后线穿插了,晕
    • threadLocals 是能够存储多个 ThreadLocal,多个存取流程同理如下

  • 总结下:通过下面的很简略的代码,就实现了线程的数据隔离,也能失去几点论断

    • ThreadLocal 对象自身是不贮存数据的,它自身的职能是执行相干的 set、get 之类的操作
    • 以后线程的实例,负责存储 ThreadLocal 的 set 操作传入的数据,其数据和以后线程的实例绑定
    • 一个 ThreadLocal 实例,在一个线程中只能贮存一类数据,前期的 set 操作,会笼罩之前 set 的数据
    • 线程中 threadLocals 是数组构造,能贮存多个不同 ThreadLocal 实例 set 的数据

Entry

  • 说到 Entry,须要先晓得下四大援用的基础知识

强援用:不论内存如许缓和,gc 永不回收强援用的对象

软援用:当内存不足,gc 对软援用对象进行回收

弱援用:gc 发现弱援用,就会立即回收弱援用对象

软援用:在任何时候都可能被垃圾回收器回收

Entry 就是一个实体类,这个实体类有俩个属性:key、value,key 是就是咱们常说的的弱援用

当咱们执行 ThreadLocal 的 set 操作,第一次则新建一个 Entry 或后续 set 则笼罩改 Entry 的 value,塞到以后 Thread 的 ThreadLocals 变量中

  • 来看下 Entry 代码

    • 此处 key 获得是 ThreadLocal 本身的实例,能够看进去 Entry 持有的 key 属性,属于弱援用属性
    • value 就是咱们传入的数据:类型取决于咱们定义的泛型
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {super(k);
        value = v;
    }
}
  • Entry 有个比拟奇妙的构造,继承弱援用类,而后本身外部又定义了一个强援用属性,使得该类有一强一弱的属性
  • 结构图

你可能会想,what?我用 ThreadLocal 来 set 一个数据,而后 gc 一下,我 Entry 外面 key 变量援用链就断开了?

  • 来试一下
public class Main {public static void main(String[] args) {ThreadLocal<String> threadLocalOne = new ThreadLocal<>();

        new Thread(new Runnable() {
            @Override
            public void run() {threadLocalOne.set("线程一的数据 --- threadLocalOne");
                System.gc();
                System.out.println(threadLocalOne.get());
            }
        }).start();}
}
  • 后果
线程一的数据 --- threadLocalOne

看来这里 gc 了个寂寞。。。

在这里,必须明确一个情理:gc 回收弱援用对象,是先回收弱援用的对象,弱援用链天然断开;而不是先断开援用链,再回收对象。Entry 外面 key 对 ThreadLocal 的援用是弱援用,然而 threadLocalOne 对 ThreadLocal 的援用是强援用啊,所以 ThreadLocal 这个对象是没法被回收的

  • 来看下下面代码真正的援用关系

  • 此处能够演示下,threadLocalOne 对 ThreadLocal 的援用链断开,Entry 外面 key 援用被 gc 回收的状况
public class Main {static ThreadLocal<String> threadLocalOne = new ThreadLocal<>();

    public static void main(String[] args) {new Thread(new Runnable() {
            @Override
            public void run() {threadLocalOne.set("线程一的数据 --- threadLocalOne");
                try {
                    threadLocalOne = null;
                    System.gc();

                    // 上面代码来自:https://blog.csdn.net/thewindkee/article/details/103726942
                    Thread t = Thread.currentThread();
                    Class<? extends Thread> clz = t.getClass();
                    Field field = clz.getDeclaredField("threadLocals");
                    field.setAccessible(true);
                    Object threadLocalMap = field.get(t);
                    Class<?> tlmClass = threadLocalMap.getClass();
                    Field tableField = tlmClass.getDeclaredField("table");
                    tableField.setAccessible(true);
                    Object[] arr = (Object[]) tableField.get(threadLocalMap);
                    for (Object o : arr) {if (o == null) continue;
                        Class<?> entryClass = o.getClass();
                        Field valueField = entryClass.getDeclaredField("value");
                        Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
                        valueField.setAccessible(true);
                        referenceField.setAccessible(true);
                        System.out.println(String.format("弱援用 key:%s    值:%s", referenceField.get(o), valueField.get(o)));
                    }
                } catch (Exception e) {}}
        }).start();}
}
  • 后果

    • key 为 null 了!下面有行代码:threadLocalOne = null,这个就是断开了对 ThreadLocal 对象的强援用
    • 大家如果有趣味的话,能够把 threadLocalOne = null 去掉,再运行的话,会发现,key 不会为空
    • 反射代码的性能就是取到 Thread 中 threadLocals 变量,循环取其中的 Entry,打印 Entry 的 key、value 值
弱援用 key:null    值: 线程一的数据 --- threadLocalOne
弱援用 key:java.lang.ThreadLocal@387567b2    值:java.lang.ref.SoftReference@2021fb3f
  • 总结

    • 大家心里可能会想,这变量始终持有强援用,key 那个弱援用可有可无啊,而且子线程代码执行工夫个别也不长
    • 其实不然,咱们能够想想 Android app 外面的主线程,就是一个死循环,以事件为驱动,外面能够搞巨多巨难的逻辑,这个强援用的变量被赋其它值就很可能了

      • 如果 key 是强援用,那么这个 Entry 外面的 ThreadLocal 根本就很难被回收
      • key 为弱援用,当 ThreadLocal 对象强援用链断开后,其很容易被回收了,相干革除算法,也能很容易清理 key 为 null 的 Entry
    • 一个弱援用都能玩出这么多花色

ThreadLocalMap 环形构造

  • 咱们来看下 ThreadLocalMap 代码

    • 先去掉一堆临时没必要关注的代码
    • table 就是 ThreadLocalMap 的次要构造了,数据都存在这个数组外面
    • 所以说,ThreadLocalMap 的主体构造就是一个 Entry 类型的数组
public class ThreadLocal<T> {
    
    ...
    
    static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {super(k);
                value = v;
            }
        }
        
        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        ...
    }
}
  • 在此处你可能又有疑难了,这货色不就是一个数组吗?怎么和环形构造扯上关系了?

  • 数组失常状况下的确是上面的这种构造

  • 然而,ThreadLocalMap 类外面,有个办法做了一个骚操作,看下代码
public class ThreadLocal<T> {
    
    ...
    
    static class ThreadLocalMap {
        ...
            
        private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);
        }
           
        ...
    }
}
  • 这个 nextIndex 办法,大家看懂了没?

    • 它的次要作用,就是将传入 index 值加一
    • 然而!当 index 值长度超过数组长度后,会间接返回 0,又回到了数组头部,这就实现了一个环形构造

  • 总结

    • 这样做有个很大的益处,可能大大的节俭内存的开销,可能充沛的利用数组的空间
    • 取数据的时候会升高一些效率,工夫置换空间

set

总流程

  • 塞数据的操作,来看下这个 set 操作的代码:上面的代码,逻辑还是很简略的

    1. 获取以后线程实例
    2. 获取以后线程中的 threadLocals 实例
    3. threadLocals 不为空执行塞值操作
    4. threadLocals 为空,new 一个 ThreadLocalMap 赋值给 threadLocals,同时塞入一个 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);
}

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

void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}
  • 须要留神的是,ThreadLocalMap 生成 Entry 数组,设置了一个默认长度,默认为:16
 private static final int INITIAL_CAPACITY = 16;

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];
    ...
}
  • 流程图

map.set

  • 下面说了一些细枝末节,当初来说说最重要的map.set(this, value) 办法
private void set(ThreadLocal<?> key, Object value) {// We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();}

取哈希值

  • 下面代码有个计算哈希值的操作

    • key.threadLocalHashCode 这行代码上来看,就如同将本身的实例计算 hash 值
    • 其实看了残缺的代码,发现传入 key,只不过是为了调用 nextHashCode 办法,用它来计算哈希值,并不是将以后 ThreadLocal 对象转化成 hash 值
public class ThreadLocal<T> {private final int threadLocalHashCode = nextHashCode();

    private static final int HASH_INCREMENT = 0x61c88647;
    
    private static AtomicInteger nextHashCode = new AtomicInteger();

    private void set(ThreadLocal<?> key, Object value) {
        ...
        int i = key.threadLocalHashCode & (len-1);
        ...
    }

    private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
}
  • 这中央用了一个原子类的操作,来看下getAndAdd() 办法的作用

    • 这就是个相加的性能,相加后返回原来的旧值,保障相加的操作是个原子性不可分割的操作
public class Main {public static void main(String[] args) {AtomicInteger atomicInteger = new AtomicInteger();
        
        System.out.println(atomicInteger.getAndAdd(1));  //0
        System.out.println(atomicInteger.getAndAdd(1));  //1
        System.out.println(atomicInteger.getAndAdd(1));  //2
    }
}
  • HASH_INCREMENT = 0x61c88647,为什么偏偏将将 0x61c88647 这个十六进制相加呢,为什么不能是 1,2,3,4,5,6 呢?

该值的相加,合乎斐波那契散列法,能够使得的低位的二进制数值散布的更加平均,这样会缩小在数组中产生 hash 抵触的次数

具体分析可查看:从 ThreadLocal 的实现看散列算法

等等大家有没有看到 threadLocalHashCode = nextHashCode(),nextHashCode()是获取下一个节点的办法啊,这是什么鬼?

难道每次应用 key.threadLocalHashCode 的时候,HashCode 都会变?

  • 看下残缺的赋值语句

    • 这是在初始化变量的时候,就间接定义赋值的
    • 阐明实例化该类的时候,nextHashCode()获取一次 HashCode 之后,就不会再次获取了
    • 加上用的 final 润饰,仅能赋值一次
    • 所以 threadLocalHashCode 变量,在实例化 ThreadLocal 的时候,获取 HashCode 一次,该数值就定下来了,在该实例中就不会再变动了
public class ThreadLocal<T> {private final int threadLocalHashCode = nextHashCode();
}

如同又发现一个问题!threadHashCode 通过 nextHashCode() 获取 HashCode,而后 nextHashCode 是应用 AtomicInteger 类型的 nextHashCode 变量相加,这玩意每次实例化的时候不都会归零吗?

难道咱们每次新的 ThreadLocal 实例获取 HashCode 的时候,都要从 0 开始相加?

  • 来看下残缺代码

    • 大家看下 AtomicInteger 类型的 nextHashCode 变量,他的润饰关键字是static
    • 这阐明该变量的数值,是和这个类绑定的,和这个类生成的实例无关,而且从始至终,只会实例化一次
    • 当不同的 ThreadLocal 实例调用 nextHashCode,他的数值就会相加一次
    • 而且每个实例只能调用一次 nextHashCode()办法,nextHashCode 数值会很平均的变动
public class ThreadLocal<T> {private final int threadLocalHashCode = nextHashCode();

    private static final int HASH_INCREMENT = 0x61c88647;
    
    private static AtomicInteger nextHashCode = new AtomicInteger();

    private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
}

总结

  • 通过寥寥数行的初始化,几个关键字,就能造成在不同实例中,都能稳步变动的 HashCode 数值
  • 这些基础知识大家或者都晓得,又有多少能这样信手拈来呢?

取 index 值

下面代码中,用获得的 hash 值,与 ThreadLocalMap 实例中数组长度减一的与操作,计算出了 index 值

这个很重要的,因为大于长度的高位 hash 值是不须要的

此处会将传入的 ThreadLocal 实例计算出一个 hash 值,怎么计算的前面再说,这中央有个位与的操作,这中央是和长度减一的与操作,这个很重要的,因为大于长度的高位 hash 值是不须要的

  • 假如 hash 值为:010110011101
  • 长度(此处抉择默认值:16-1):01111
  • 看下图可知,这个与操作,可去掉高位无用的 hash 值,取到的 index 值可限度在数组长度中

塞值

  • 看下塞值进入 ThreadLocalMap 数组的操作

    • 对于 Key:因为 Entry 是继承的 WeakReference 类,get()办法是获取其外部弱援用对象,所以能够通过 get()拿到以后 ThreadLocal 实例
    • 对于 value:间接 .value 就 OK 了
private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];  e != null; e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    ...
}

剖析下塞值流程

  • 实际上面的循环还值得去思考,来思考下这循环解决的事件
  • 循环中获取以后 index 值,从 Entry 数组取到以后 index 地位的 Entry 对象
  1. 如果获取的这 Entry 是 null,则间接完结这个循环体

    • 在 Entry 数组的 index 塞入一个新生成的节点
  2. 如果获取的这 Entry 不为 null

    1. key 值相等,阐明 Entry 对象存在,笼罩其 value 值即可
    2. key 为 null,阐明该节点可被替换(替换算法前面讲),new 一个 Entry 对象,在此节点存储数据
    3. 如果 key 不相等且不为 null,循环获取下一节点的 Entry 对象,并反复上述逻辑

整体的逻辑比拟清晰,如果 key 已存在,则笼罩;不存在,index 地位是否可用,可用则应用该节点,不可用,往后寻找可用节点:线性探测法

  • 替换旧节点的逻辑,切实有点绕,上面独自提出来阐明

替换算法

在上述 set 办法中,当生成的 index 节点,已被占用,会向后探测可用节点

  • 探测的节点为 null,则会间接应用该节点
  • 探测的节点 key 值雷同,则会笼罩 value 值
  • 探测的节点 key 值不雷同,持续向后探测
  • 探测的节点 key 值为 null,会执行一个替换旧节点的操作,逻辑有点绕,上面来剖析下
private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];  e != null; e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();
        ...
        if (k == null) {replaceStaleEntry(key, value, i);
            return;
        }
    }
    ...
}
  • 来看下 replaceStaleEntry 办法中的逻辑
private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1);
}

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // Find either the key or trailing null slot of run, whichever
    // occurs first
    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        if (k == key) {
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
  • 下面的代码,很显著俩个循环是重点逻辑,这外面有俩个很重要的字段:slotToExpunge 和 staleSlot

    • staleSlot:记录传进来节点 key 为 null 的地位
    • slotToExpunge:标定是否须要执行最初的清理办法
  • 第一个循环:很显著是往前列表头结点方向探测,是否还有 key 为 null 的节点,有的话将其下标赋值给 slotToExpunge;这个探测是一个间断的不为 null 的节点链范畴,有空节点,立马完结循环

  • 第二个循环:很显著次要是向后探测,探测整个数组,这里有很重要逻辑

    • 这中央曾经开始有点绕了,我 giao,大家要好好想想
    • 当探测的 key 和传入的须要设值的 key 雷同时,会复写探测到 Entry 的 value,而后将探测到地位和传入地位,俩者互相调换
  • 为什么会呈现探测到 Entry 和传入 key 雷同?

    • 雷同是因为,存在到数组的时候,产生了 hash 抵触,会主动向后探测适合的地位存储
    • 当你第二次用 ThreadLocal 存值的时候,hash 产生的 index,比拟俩者 key,必定是不可能雷同,因为产生了 hash 抵触,真正贮存 Entry,在往后的地位;所以须要向后探测
    • 假如探测的时候,始终没有遇到 key 为 null 的 Entry:失常循环的话,必定是能探测到 key 雷同的 Entry,而后进行复写 value 的操作
    • 然而在探测的时候,遇到 key 为 null 的 Entry 的呢?此时就进入了替换旧 Entry 算法,所以替换算法就也有了一个向后探测的逻辑
    • 探测到雷同 key 值的 Entry,就阐明了找到了咱们须要复写 value 的 Entry 实例
  • 为什么要调换俩者地位呢?

    • 这个问题,大家能够好好想想,咱们时候往后探测,而这 key 为 null 的 Entry 实例,属于较快的探测到 Entry
    • 而这个 Entry 实例的 key 又为 null,阐明这个 Entry 能够被回收了,此时正处于占着茅坑不拉屎的地位
    • 此时就能够把我须要复写 Entry 实例和这个 key 为 null 的 Entry 调换地位
    • 能够使得咱们须要被操作的 Entry 实例,在下次被操作时候,能够尽快被找到
  • 调换了地位之后,就会执行擦除旧节点算法

  • 下面是探查间断的 Entry 节点,未碰到 null 节点的状况;如果碰到 null 节点,会间接完结探测

    • 请留神,如果数组中,有须要复写 value 的节点;在计算的 hash 值处,向后探测的过程,肯定不会碰到 null 节点
    • 毕竟,第一次向后探测可用节点是,碰到第一个 null 节点,就停下来应用了

  • 在第二个循环中,还有一段代码,比拟有意思,这判断逻辑的作用是

    • 以 key 为 null 的 Entry,以它为界线
    • 向前探测的时候,未碰到 key 为 null 的 Entry
    • 而向后探测的时候,碰到的 key 为 null 的 Entry
    • 而后扭转 slotToExpunge 的值,使其和 staleSlot 不相等
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
       ...
    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ...
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
    ...
}

  • 能够看进去这俩个循环的操作,是有关联性,对此,我示意

为什么这俩个循环都这么执着的,想扭转 slotToExpunge 的数值呢?

  • 来看下对于 slotToExpunge 的要害代码
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    ...
    int slotToExpunge = staleSlot;
    ...
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

明确了吧!都是为了替换办法外面的最初一段逻辑:为了判断是否须要执行 擦除算法

总结

  • 双向探测流程

    • 替换算法会以传入的 key 为 null 的 Entry 节点为界线,在一个间断的 Entry 范畴往俩边探测

      • 什么是间断的 Entry 范畴?这边数组的节点都不能为 null,碰到为 null 节点会完结探测
    • 先向前探测:如果碰到 key 为 null 的 Entry,会将其下标赋值给slotToExpunge
    • 向后探测使:如果向前探测没有碰到 key 的节点,只有向后探测的时候碰到为 null 的节点,会将其下标赋值给slotToExpunge
    • 上面向俩边探测的逻辑,是为了:遇到 key 为 null 的节点,能确保 slotToExpunge 不等于 staleSlot
  • 在向后探测的时候,如果遇到 key 值比照雷同的 Entry,阐明遇到咱们须要复写 value 的 Entry

    • 此时复写 value 的 Entry,用咱们传入的 value 数值将其原来的 value 数值笼罩
    • 而后将传入 key 为 null 的 Entry(通过传入的下标得悉 Entry)和须要复写 value 的 Entry 替换地位
    • 最初执行擦除算法
  • 如果在向后探测的时候,没有遇到遇到 key 值比照雷同的 Entry

    • 传入 key 为 null 的 Entry,将其 value 赋值为 null,断开援用
    • 创立一个新节点,放到此地位,key 为传入以后 ThreadLocal 实例,value 为传入的 value 数据
    • 而后依据 lotToExpunge 和 staleSlot 是否相等,来判断是否要执行擦除算法

总结

来总结下

  • 再来看下总流程

  • 下面剖析完了替换旧节点办法逻辑,终于能够把 map.set 的那块替换算法操作流程补起来了

    • 不论后续遇到 null,还是遇到须要被复写 value 的 Entry,这个 key 为 null 的 Entry 都将被替换掉

这俩个图示,大略形容了 ThreadLocal 进行 set 操作的整个流程;当初,进入下一个栏目吧,来看看 ThreadLocal 的 get 操作!

get

get 流程,总体要比 set 流程简略很多,能够轻松一下了

总流程

  • 来看下代码

    • 总体流程非常简单,将本身作为 key,传入 map.getEntry 办法,获取合乎实例的 Entry,而后拿到 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();}
  • 如果通过 map.getEntry 获取的 Entry 为 null,会返回setInitialValue(),来看下这个办法是干嘛的

    • 从这个办法可知,如果咱们没有进行 set 操作,间接进行 get 操作,他会给 ThreadLocal 的 threadLocals 办法赋初值
    • setInitialValue() 办法,返回的是initialValue() 办法的数据,可知默认为 null
    • 所以通过 key 没查到对应的 Entry,get 办法会返回 null
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;}

void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}

map.getEntry

  • 从下面的代码能够看进去,getEntry 办法是获取符合条件的节点

    • 这里逻辑很简略,通过以后 ThreadLocal 实例获取 HashCode,而后算出 index 值
    • 间接获取以后 index 下标的 Entry,将其 key 和以后 ThreadLocal 实例比照,看是否一样
    • 雷同:阐明没有产生 Hash 碰撞,能够间接应用
    • 不雷同 :阐明产生了 Hash 碰撞,须要向后探测寻找,执行 getEntryAfterMiss() 办法
    • 此时,就须要来看看 getEntryAfterMiss()办法逻辑了
private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

getEntryAfterMiss

  • 来看下代码
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;
    int len = tab.length;

    while (e != null) {ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

整体逻辑还是很清晰了,通过 while 循环,一直获取 Entry 数组中的下一个节点,循环中有三个逻辑走向

  1. 以后节点的 key 等于以后 ThreadLocal 实例:间接返回这个节点的 Entry
  2. 以后节点的 key 为 null:执行擦除旧节点算法,持续循环
  3. 以后节点的能够不等于以后 ThreadLocal 实例且不为 null:获取下一节点的下标,而后持续下面的逻辑
  • 如果没有获取到符合条件的 Entry 节点,会间接返回 null

总结

ThreadLocal 的流程,总体上比较简单

  • 将以后 ThreadLocal 实例当为 key,查找 Entry 数组以后节点(应用 ThreadLocal 中的魔术值算出的 index)是否符合条件
  • 不符合条件将返回 null

    • 从未进行过 set 操作
    • 未查到符合条件的 key
  • 符合条件就间接返回以后节点

    • 如果遇到哈希抵触,算出的 index 值的 Entry 数组上存在 Entry,然而 key 不相等,就向后查找
    • 如果遇到 key 为 null 的 Entry,就执行擦除算法,而后持续往后寻找
    • 如果遇到 key 相当的 Entry,就间接完结寻找,返回这个 Entry 节点
  • 这里大家肯定要明确一个概念:在 set 的流程,产生了 hash 抵触,是在抵触节点向后的间断节点上,找到符合条件的节点存储,所以查问的时候,只须要在间断的节点上查找,如果碰到为 null 的节点,就能够间接完结查找

擦除算法

在 set 流程和 get 流程都应用了这个擦除旧节点的逻辑,它能够及时革除掉 Entry 数组中,那些 key 为 null 的 Entry,如果 key 为 null,阐明这些这节点,曾经没中央应用了,所以就须要革除掉

  • 来看看这个办法代码
private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter 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;
}

前置操作

从下面的代码,能够发现,再进行次要的循环体,有个前置操作

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

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    ...
}
  • 这中央做了很简略的置空操作,如果 Entry 节点的 key 为空,阐明这个节点能够被革除,value 置空,和数组的链接断开

<img src=”https://cdn.jsdelivr.net/gh/CNAD666/MyData@master/pic/flutter/blog/20210506095408.png” alt=” 擦除算法 - 前置操作 ” style=”zoom: 70%;” />

主体逻辑

  • 很显著,循环体外面的逻辑是最重要,而且循环体外面做了一个相当乏味的操作!
private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;
    int len = tab.length;
    ...
    // Rehash until we encounter 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;
}
  • 下面的循环体外面,就是一直的获取下一节点的 Entry 实例,而后判断 key 值进行相干解决
  • key 为 null:中规中矩的,将 value 置空,断开与数组的链接
  • key 不为 null:这时候就有意思了

    • 首先,会获取以后 ThreadLocal 实例的 hash 值,而后获得 index 值
    • 判断 h(idnex 值)和 i 是否相等,不相等进行下述操作,因为 Entry 数组是环形构造,是实现存在相等的状况

      1. 会将以后循环到节点置空,该节点的 Entry 记为 e
      2. 从通过 hreadLocal 实例的 hash 值获取到 index 处,开始进行循环
      3. 循环到节点 Entry 为 null,则完结循环
      4. 将 e 赋值给为 null 的节点
    • 这外面的逻辑就是要害了
  • 大家可能对这个文字的形容,感觉比拟形象,来个图,来领会下这短短几行代码的妙处

总结

代码很少,然而实现的性能却并不少

  • 擦除旧节点的办法,在 Entry 上探测的时候

    • 遇到 key 为空的节点,会将该节点置空
    • 遇到 key 不为空的节点,会将该节点移到靠前地位(具体挪动逻辑,请参考上述阐明)
  • 交互到靠前节点地位,能够看出,次要的目标,是为了:

    • ThreadLocal 实例计算出的 index 节点地位往后的地位,能让节点放弃连续性
    • 也能让替换的节点,更快的被操作

扩容

在进行 set 操作的时候,会进行相干的扩容操作

  • 来看下扩容代码入口:resize 办法便是扩容办法
public void set(T value) {
    ...
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

private void set(ThreadLocal<?> key, Object value) {
    ...
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();}

private void rehash() {expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();}
  • 来看下扩容代码
private void resize() {Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {Entry e = oldTab[j];
        if (e != null) {ThreadLocal<?> k = e.get();
            if (k == null) {e.value = null; // Help the GC} else {int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}

触发条件

先来看下扩容的触发条件吧

  • 整体代码
public void set(T value) {
    ...
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

private void set(ThreadLocal<?> key, Object value) {
    ...
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();}

private void rehash() {expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();}

下面次要的代码就是:!cleanSomeSlots(i, sz) && sz >= threshold

  • 来看下 threshold 是什么

    • 只有 Entry 数组含有 Entry 实例大于等于数组的长度的三分之二,便能满足后一段断定
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

private void setThreshold(int len) {threshold = len * 2 / 3;}
  • 来看看前一段的断定,看下cleanSomeSlots,只有返回 false,就能触发扩容办法了
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ((n >>>= 1) != 0);
    return removed;
}

n >>>= 1:表白是无符号右移一位,负数高位补 0,正数高位补 1

举例:0011 —> 0001

在下面的 cleanSomeSlots 办法中,只有在探测节点的时候,没有遇到 Entry 的 key 为 null 的节点,该办法就会返回 false

  • rehash 办法就非常简单了

    • 执行擦除办法
    • 只有 size(含有 Entry 实例数)长度大于等于 3 /4 threshold,就执行扩容操作
private void rehash() {expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();}

总结

满足上面俩个条件即可

  1. Entry 数组中不含 key 为 null 的 Entry 实例
  2. 数组中含有是实例数大于等于 threshold 的四分之三(threshold 为数组长度的 三分之二)

扩容逻辑

private void resize() {Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {Entry e = oldTab[j];
        if (e != null) {ThreadLocal<?> k = e.get();
            if (k == null) {e.value = null; // Help the GC} else {int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}
  • 从下面的逻辑,能够看进去,将旧数组的数据赋值到扩容数组,并不是全盘赋值到扩容数组的对应地位
  • 遍历旧数组,取出其中的 Entry 实例

    • key 为 null:须要将该节点 value 置空,期待 GC 解决(Help the GC,hhhh)

      • 这里你可能有个疑难,不是说数组的节点 key 不为 null,才会触发扩容机制吗?
      • 在多线程的环境里,执行扩容的时候,key 的强援用断开,导致 key 被回收,从而 key 为 null,这是齐全存在的
    • key 不为 null:算出 index 值,向扩容数组中存储,如果该节点抵触,向后找到为 null 的节点,而后存储
  • 这里的扩容存储和 ArrayList 之类是有区别

总结

能够发现

  • set,替换,擦除,扩容,根本无时无刻,都是为了使 hash 抵触节点,向抵触的节点凑近
  • 这是为了进步读写节点的效率

remove

remove 办法是非常简单的,ThreadLocal 领有三个 api:set、get、remove;尽管非常简单,然而还有一些必要,来略微理解下

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

private void remove(ThreadLocal<?> key) {Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

逻辑十分的清晰,通过 ThreadLocal 实例,获取以后的 index,而后从此开始查找符合条件 Entry,找到后,会将其 key 值清掉,而后执行擦除算法

e.clear 就是,弱援用的清理弱援用的办法,很简略,将弱援用 referent 变量置空就行了,这个变量就是持有弱援用对象的变量

<img src=”https://cdn.jsdelivr.net/gh/CNAD666/MyData@master/pic/flutter/blog/20210506095436.png” alt=”remove 流程 ” style=”zoom: 33%;” />

最初

文章写到这里,基本上到了序幕了,写了差不多万余字,心愿大家看完后,对 ThreadLocal 能有个更加深刻的意识

ThreadLocal 的源码尽管并不多,然而其中有很多奇思妙想,有种萝卜雕花的感觉,这就是高手写的代码吗?

系列文章

  • 源码篇:Handler 那些事(万字图文)

正文完
 0