前言
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操作的代码:上面的代码,逻辑还是很简略的
- 获取以后线程实例
- 获取以后线程中的threadLocals实例
- threadLocals不为空执行塞值操作
- 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对象
如果获取的这Entry是null,则间接完结这个循环体
- 在Entry数组的index塞入一个新生成的节点
如果获取的这Entry不为null
- key值相等,阐明Entry对象存在,笼罩其value值即可
- key为null,阐明该节点可被替换(替换算法前面讲),new一个Entry对象,在此节点存储数据
- 如果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数组中的下一个节点,循环中有三个逻辑走向
- 以后节点的key等于以后ThreadLocal实例:间接返回这个节点的Entry
- 以后节点的key为null:执行擦除旧节点算法,持续循环
- 以后节点的能够不等于以后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数组是环形构造,是实现存在相等的状况
- 会将以后循环到节点置空,该节点的Entry记为e
- 从通过hreadLocal实例的hash值获取到index处,开始进行循环
- 循环到节点Entry为null,则完结循环
- 将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();}
总结
满足上面俩个条件即可
- Entry数组中不含key为null的Entry实例
- 数组中含有是实例数大于等于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那些事(万字图文)