持续积累ThreadLocal技术的目录
- 一、先从使用ThreadLocal开始
- 1、我看到的两种创建方式
- 1.1 ThreadLocal<A> aThreadLocal = new ThreadLocal<>();
- 1.2 ThreadLocal<A> aThreadLocal = ThreadLocal.withInitial(...)
- 1.3 为啥需要1.2提到的创建方式?直接new不就好了?
- 2、创建好了后,在使用aThreadLocal时,会涉及到3种重要的API
- 2.0 在理解3种API之前,又不得不了解下ThreadLocal内部的存储结构(不清楚存储结构,何谈对其的操作?)
- 2.0.1 3种API的概述
- 2.0.2 ThreadLocal内部的存储结构
- 2.1 set方法:aThreadLocal.set(...)
- 2.1.1 JDK源码
- 2.1.1.1 重点关注:map.set(this, value); 实现原理
- 如何查找?(哈希表的思路)
- 调用rehash()方法的前提条件,以及该方法的原理
- 2.1.1.2 关注:createMap(t, value); 实现原理
- 2.2 get方法:aThreadLocal.get()
- 2.2.1 JDK源码
- 2.2.1.1 重点关注:map.getEntry(this); 实现原理
- 2.3 remove方法:aThreadLocal.remove()
- 2.3.1 JDK源码
- 2.3.1.1 重点关注:m.remove(this); 实现原理
- 附录
- 1、ThreadLocal提供的set()、get()、remove()方法,都有一个getMap()方法。在这里补充说明下。
- 1.1 重点关注:getMap()方法的原理【ThreadLocal.java中的方法】
- 最后
一、先从使用ThreadLocal开始
1、我看到的两种创建方式
1.1 ThreadLocal aThreadLocal = new ThreadLocal<>();
1.2 ThreadLocal aThreadLocal = ThreadLocal.withInitial(…)
1.3 为啥需要1.2提到的创建方式?直接new不就好了?
- JDK源码中,就提到了ThreadLocal.withInitial(…):
2、创建好了后,在使用aThreadLocal时,会涉及到3种重要的API
2.0 在理解3种API之前,又不得不了解下ThreadLocal内部的存储结构(不清楚存储结构,何谈对其的操作?)
2.0.1 3种API的概述
- set方法:aThreadLocal.set(…)
- get方法:aThreadLocal.get()
- remove方法:aThreadLocal.remove()
2.0.2 ThreadLocal内部的存储结构
- 图解:
2.1 set方法:aThreadLocal.set(…)
2.1.1 JDK源码
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
- 逻辑概述:一个线程Thread0去执行aThreadLocal.set(…)时,首先会取出自己的ThreadLocalMap,如果该map不存在,则创建并存储(ThreadLocal对象,value)。如果map存在,则直接存储(ThreadLocal对象,value)。
- 对getMap(t)的解读见:“附录 1、ThreadLocal提供的set()、get()、remove()方法,都有一个getMap()方法。在这里补充说明下。”
2.1.1.1 重点关注:map.set(this, value); 实现原理
- 方法签名:
private void set(ThreadLocal<?> key, Object value) {......
}
不贴完整源码了,毕竟在csdn上看源码并不是一个明智的决定~
对着IDEA上的源码,看本篇文章,才是不错的选择哟~
- 思想:既然map(ThreadLocalMap)的存储结构是Entry[] table; 那显然要在table中找到一个位置table[i],将<key, value>放进去。
- 如何查找呢?
(1)简单的办法:遍历table。很显然JDK的设计者没有采用这种低效的方式。
(2)哈希表的思路(学《数据结构与算法》的作用体现出来了!):对key求一个hash值,并将其转换为数组中的合法下标。
实践:index = hash(key) % capacity (index = hash(key) & (capacity - 1) )更高效
6.1.2 哈希表简单实现 有必要学一学的~
如何查找?(哈希表的思路)
- 回到set方法的源码:
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
1)len是2的幂,
len - 1
表示成2进制,就是1…1,那么x & 1…1的范围一定是[0, 1…1],也就是数组下标的合法范围。
2)len不是2的幂,x & (len - 1)的范围也是[0, len - 1]。因为,len - 1
表示为二进制,与它相与,最大情况下,便是1都没变成0,那么就是len - 1
。因此,范围也是[0, len - 1]
- 找到要放的位置了,但可能这个坑位已经被别人占了啊,这咋办?–> 遍历找下一个i。
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;}}
1)如果找到的位置,就是自己蹲过的坑(if (k == key) ),那么更新value即可。
2)如果找到的位置,是别人曾经蹲过的坑,但现在他跑路了,空出来的坑(if (k == null)),那么赶紧占这个坑即可:replaceStaleEntry(key, value, i); 【这种情况要多思考一下,Entry对象不为null,但key为null。这是不是ThreadLocal对象.remove()后的情况啊?】
看到remove方法就懂了~
-
承接上文,第3种情况,找到一个从未有人蹲过的坑,那咱来蹲这个坑啊,即创建Entry对象(tab[i] = new Entry(key, value);)
-
创建了Entry对象,意味着Entry数组中被蹲过的坑多了一个(int sz = ++size;),如果一直就这么发展下去,留给后人的坑就少了,那大家就容易内卷了,这不就冲突多了?这可不行啊。因此,有必要来消杀一波:
if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
调用rehash()方法的前提条件,以及该方法的原理
持续更新…
2.1.1.2 关注:createMap(t, value); 实现原理
- 源码:
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);
}
- 如果一个线程对象Thread的threadLocals为null,那么要new一个ThreadLocalMap对象。在这个过程中,table被初始化为Entry[16]的数组(INITIAL_CAPACITY = 16,必须是2的幂)。
- 找一个位置table[i],放入<firstKey, firstValue>
- setThreshold(INITIAL_CAPACITY); 会进行计算:threshold = len * 2 / 3; 也就是将threshold赋值为10。
2.2 get方法:aThreadLocal.get()
2.2.1 JDK源码
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();}
- 逻辑概述:代码结构和set()很像,都是先根据线程对象t,找到自己的ThreadLocalMap对象,如果map为null,那设置一个初始化的value值
x
并返回。如果map不为null,那么根据key(ThreadLocal对象)去Entry[]数组里面,找到自己的Entry对象。找不到,则返回value值x
,否则返回Entry对象中的value值。
2.2.1.1 重点关注:map.getEntry(this); 实现原理
- 方法的源码(比较短,贴一下)
private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);
}
- 相比map.set(this, value),getEntry()方法简单一些。
(1)同样,先在Entry数组中找到一个位置,如果这个坑位就是自己的,那直接:return e;。
(2)如果找不到自己的坑位(e == null || e.get() != key),那么:return getEntryAfterMiss(key, i, e);
暂不研究:getEntryAfterMiss方法。目前不影响理解ThreadLocal原理。
2.3 remove方法:aThreadLocal.remove()
2.3.1 JDK源码
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);
}
哈哈,JDK的设计者也有搬砖的体验啊,3个方法的代码结构基本一样。
之前都这么写的:
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {}return xxx;
- 发现太搬砖了,就改成了:上面的写法。写简略点,早点下班回家吧~
2.3.1.1 重点关注: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;}}
}
- 这里面的很多代码都在
2.1.1.1 重点关注:map.set(this, value); 实现原理
中见过:
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)]) {......}
}
不管是set还是remove,在操作之前,都要找到要操作的Entry对象。
- 在remove方法中,找到这个Entry对象后,如果这个坑里面搬砖的人是key,那么执行:e.clear(); 以及expungeStaleEntry(i);
expungeStaleEntry(i); 先不管这个方法,目前不影响理解ThreadLocal原理。
- 再看下e.clear();
public void clear() {this.referent = null;
}
1)在
2.1.1.1 重点关注:map.set(this, value); 实现原理
中,提到:如果找到的位置,是别人曾经蹲过的坑,但现在他跑路了,空出来的坑(if (k == null)),那么赶紧占这个坑即可:replaceStaleEntry(key, value, i); 【这种情况要多思考一下,Entry对象不为null,但key为null。这是不是ThreadLocal对象.remove()后的情况啊?】,在这里,我们可以有一个结论
了:是的,在remove()方法中,会将key置为null。
附录
1、ThreadLocal提供的set()、get()、remove()方法,都有一个getMap()方法。在这里补充说明下。
- 前提:我们知道ThreadLocal对象是和线程绑定的。线程为了组织<ThreadLocal对象,value>,提供了ThreadLocalMap。因此,咱先要找到线程的ThreadLocalMap对象。
- 代码:
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
或者直接:
ThreadLocalMap m = getMap(Thread.currentThread());
1.1 重点关注:getMap()方法的原理【ThreadLocal.java中的方法】
- JDK源码
ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}
Thread.java中,对threadLocals的定义为:ThreadLocal.ThreadLocalMap threadLocals = null;
而ThreadLocal.java和Thread.java在同一个包下:java.lang。因此,可以直接t.threadLocals。
最后
- 2024-01-07 (1)重点了解了Thread持有的ThreadLocalMap的存储结构,本质是Entry[]数组。(2)还了解了ThreadLocal提供的3种重要且常用的API:set(…)、get()、remove()。
- 之后还需要持续更新:(1)继续深挖ThreadLocal的其他原理,例如rehash()方法的内部细节。ThreadLocal存在的坑点,以及如何以最佳实践的方式来使用ThreadLocal。
- 这并不是一篇传授知识的文章,而是一起学习的产物,写的不对的地方,希望大家在评论区指出,我看到后,会修正为正确的结论~
比心