前语

ThreadLocal的文章在网上也有不少,可是看了一些后,了解起来总感觉有绕,并且看了ThreadLocal的源码,无论是线程阻隔、类环形数组、弱引证结构等等,实在是太有意思了!我有必要也要让咱们全面感受下其间所蕴含的那些奇思妙想! 所以这儿我想写一篇超几儿通俗易懂解析ThreadLocal的文章,相关流程会运用许多图示解析,以证明:我是干货,不是水比!

ThreadLocal这个类加上巨大的注释,总共也才七百多行,并且你把这个类的代码复制出来,你会发现,它几乎没有报错!耦合度极低!(唯一的报错是由于ThreadLocal类引证了Thread类里的一个包内可见变量,所以把代码复制出来,这个变量访问就报错了,只是只有此处报错!)

ThreadLocal的线程数据阻隔,替换算法,擦除算法,都是有必要去了解了解,只是少量的代码,却能完结如此精妙的功用,让咱们来领会下 Josh Bloch 和 Doug Lea 俩位大神,巧夺天工之作吧!

一些阐明

这篇文章画了不少图,大约画了十八张图,关于替换算法和擦除算法,这俩个办法所做的工作,假如不画图,光用文字描述的话,十分的笼统且很难了解;期望这些流程图,能让咱们更能领会这些精粹代码的魅力!

image-20210506091320057

运用

哔哔原理之前,有必要要先来看下运用

  • 运用起来出奇的简略,只是运用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有个比较巧妙的结构,承继弱引证类,然后本身内部又界说了一个强引证特点,使得该类有一强一弱的特点
  • 结构图

Entry结构

你或许会想,what?我用ThreadLocal来set一个数据,然后gc一下,我Entry里边key变量引证链就断开了?

img

  • 来试一下
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这个目标是没法被收回的

  • 来看下上面代码真实的引证联系

Entry的key值引证链

  • 此处能够演示下,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
    • 一个弱引证都能玩出这么多花样

img

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;
...
}
}
  • 在此处你或许又有疑问了,这东西不便是一个数组吗?怎么和环形结构扯上联系了?

img

  • 数组正常状况下确实是下面的这种结构

UML时序图

  • 可是,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,又回到了数组头部,这就完结了一个环形结构

Entry结构变形

  • 总结
    • 这样做有个很大的优点,能够大大的节省内存的开销,能够充分的运用数组的空间
    • 取数据的时分会降低一些功率,时刻置换空间

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];
...
}
  • 流程图

set总流程

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数值
  • 这些基础知识咱们或许都知道,又有多少能这样信手拈来呢?

img

取index值

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

这个很重要的,由于大于长度的高位hash值是不需求的

此处会将传入的ThreadLocal实例核算出一个hash值,怎么核算的后边再说,这当地有个位与的操作,这当地是和长度减一的与操作,这个很重要的,由于大于长度的高位hash值是不需求的

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

hash值低位与操作

塞值

  • 看下塞值进入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方位是否可用,可用则运用该节点,不可用,往后寻觅可用节点:线性勘探法

  • 替换旧节点的逻辑,实在有点绕,下面单独提出来阐明

map.set流程

替换算法

在上述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抵触,会自动向后勘探合适的方位存储
    • 当你第2次用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节点,就停下来运用了

替换算法-后勘探(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;
}
...
}

替换算法-后勘探(寻觅key为null)

  • 能够看出来这俩个循环的操作,是有关联性,对此,我表明

img

为什么这俩个循环都这么执着的,想改动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是否持平,来判别是否要履行擦除算法

总结

来总结下

  • 再来看下总流程

set总流程

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

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

map.set流程(完善)

这俩个图示,大约描述了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

get流程-getEntryAfterMiss

总结

ThreadLocal的流程,全体上比较简略

  • 将当时ThreadLocal实例当为key,查找Entry数组当时节点(运用ThreadLocal中的魔术值算出的index)是否契合条件

  • 不契合条件将回来null

    • 从未进行过set操作
    • 未查到契合条件的key
  • 契合条件就直接回来当时节点

    • 假如遇到哈希抵触,算出的index值的Entry数组上存在Entry,可是key不持平,就向后查找
    • 假如遇到key为null的Entry,就履行擦除算法,然后持续往后寻觅
    • 假如遇到key适当的Entry,就直接完毕寻觅,回来这个Entry节点
  • 这儿咱们一定要明晰一个概念:在set的流程,发生了hash抵触,是在抵触节点向后的接连节点上,找到契合条件的节点存储,所以查询的时分,只需求在接连的节点上查找,假如碰到为null的节点,就能够直接完毕查找

get流程

擦除算法

在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置空,和数组的链接断开

擦除算法-前置操作

主体逻辑

  • 很明显,循环体里边的逻辑是最重要,并且循环体里边做了一个适当有趣的操作!
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变量置空就行了,这个变量便是持有弱引证目标的变量

remove流程

最终

文章写到这儿,基本上到了结尾了,写了差不多万余字,期望咱们看完后,对ThreadLocal能有个愈加深化的认识

ThreadLocal的源码尽管并不多,可是其间有许多奇思妙想,有种萝卜雕花的感觉,这便是高手写的代码吗?

img

系列文章

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