前言

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线程一的数据 --- threadLocalTwonullnull线程二的数据 --- 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那些事(万字图文)