最近看到大佬写了 threadlocal, 我觉得, 也可以跟风水一篇.
先解释下, ThreadLocal 的小用途之一.
在不增加函数参数的前提下, 可以通过ThreadLocal
来提供一个上下文(context)
,隐式增加一个参数
.比如写成这样.
String calcTitle(String a, String b) {
return thl.get() + a + b;
}
在外部维护一个叫thl
的ThreadLocal
就可以.线程里的任何函数, 都可以获取到
static final ThreadLocal<String> thl = new ThreadLocal<>();
这位技术好的客官就问了.
客: 我直接写一个map=new ConcurrentHashMap<Thread,String>()
里面, 共享这个map
. 在方法里写:
String calcTitle2(String a, String b) {
return map.get(Thread.currentThread()) + a + b;
}
不是也一样用?
K: 没毛病, 看起来一点毛病没有. 但是, 你这个手动调用了一次Thread.currentThread()
.就算不提这个接口设计问题, 你这个map
会越来越大. 而且特别是很多 Thread 都是很快就结束了生命, 但是还在这个 map 里留着.
这位听众技术是真好, 一听.
客: 嗨, 那不要紧, 我可以用完手动map.remove(Thread.currentThread())
一下.
K: 好, 很好. 但是我这个方法一个线程内, 要循环调用. 手动删是不行的,后面的就读不到了.
客: 啊这, 啊这. 没事, 还有办法. Java 引用, 分为强
,软
,弱
,虚
, 四大要诀.
-
虚
就是PhantomReference
, 是个鬼👻引用
,用不了 -
强
就是Reference
, 正是我们需要解决的问题. -
软
是SoftReference
, 是垃圾回收
&内存不足
时候会清理 -
弱WeakReference
,垃圾回收
时候,就会自动给回收了.
这地方, 我用一个弱
来做改造怎么样?
K: 不错不错. 那么, 我如果有多个值要放呢? 一个线程里有好几个线程本地变量, 怎么办.
客: 啊这, 啊这. 没事, 我把Map
设计成 Map<Thread,Map<String,Object>
, 取出内部Map
,再取一次就行.
K: 鼓掌 👏🏻!!! 客官,你已经自己实现了一个 ThreadLocal 功能了,除了接口设计的非常难看.都到这份上了, 你为啥不用官方功能, 要自己造个方轮子呢?
客: 啊这.
ThreadLocal 历史版本研究
虽然刚才的客官设计了一个接口不好看的方轮子, 但是他确实写出了,ThreadLocal 的基本功能.这功能,看起来,好像也没啥复杂的, 就是一个以当前线程为 Key 的 HashMap. 那么, 真的是这样吗?
功能大部分人都会写, 但是接口设计到大部分人都能用,都觉得好用, 却是个很难的事. 我们接下来, 就从接口设计的角度, 来研究下 ThreadLocal. 老规矩, 5W 先走起.
who, when and where 谁什么时候写了这个类
源码可以告诉我们
Since:
1.2
Author:
Josh Bloch and Doug Lea
又是Doug Lea
和Josh Bloch
, JAVA 1.2 时代引入的类.
what 怎么引入的
这就遇到一个问题了, 当前的 ThreadLocal 写法里, 用到了AtomicInteger
. 这玩意是, java1.5 引入到 Java 里的, 不可能出现在 1.2 时代. 所以, 现在看到的版本, 不是最初版本.
找到时光机, 来看看这些变化.
1.8 引入 Supplier
1.6 版本引入 AtomicInteger
我现在每个 jdk 只有一个大版本,如果是在 1.5 后期引入的话, 我也看不出来. 反正是 6 肯定有了这个.

1.5 版本引入泛型

1.2 版本里的 ThreadLocal 长什么样.
额, 这个我就不贴了. 只说结论, 其实,没什么大变化.
变化总结
这个类写的非常成功, 泛型和 Supplier 的适配, 属于实现变化. 唯一一个大变动, 是 1.5->1.6 的,引入AtomicInteger
, 修改了nextHashCode
的实现. 但是, 这也只是修改实现. 从有锁版本的synchronized
方法,换成了基于的CAS
的方法. 常量0x61c88647
到现在都没有变化过.
大成至圣先师孔子曾经说过, 计算机代码里, 不变的才是核心, 变的都是手段. 我们来看看这些个不变的.
ThreadLocal 核心不变量解析
如何根据 Thread 找到他的线程本地变量们
先绕回到, 刚刚客官设计的方轮子上. 他用了两个Map
,内部 Map 用来存放线程相关的本地变量, 外部 Map 用来获取一个线程对应的内部 Map. 然而, 这个外部Map
真的需要存在吗?
根据一个 Key 获取对应值得第一思路,当然是 的Hash
技术.然而, 亚圣曾经说过:行有不得反求诸己 《孟子·离娄上》
.如果我们能直接从Thread
上读到内部map
, 岂不是更快?
只要你面子足够大, 能跟 JDK 开发人员说上话, 让他们把本地变量的Map
直接给放到Thread
类型里,就可以.(或者你本身就是 JDK 开发者).
++ ThreadLocal.ThreadLocalMap threadLocals = null;
就是这么的刺激, 任性. 如何根据Thread
来找到内层Map
的问题, 已经解决了.
给每个 Thread 都加一个 Map?
我内部 3000 线程, 只有一瓢要使用thread local variable
的. 你给他们每人加了个 map? 内存不要钱吗? 这么豪横的么? 我砍死你.
大神出手, 当然不会犯这种低级错误. 这个属性, 是 lazy 的. 俗话说的叫, 延迟加载. 你不用是不会产生这个属性的,就 null 着.
如何给属性做延时加载
“等等, 这题我会啊!” 客官又叫了起来, “当年老子写单例时候, 什么懒汉,饿汉,双重检查锁,静态内部类,全都是滚瓜烂熟”.
K: 是滴, 很好. 不过这里没有那么复杂, 默认是 null, 用到时候,如果不是 null, new 一个就行. 这里没有线程安全问题.(想想为什么没有线程安全问题?)
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
客: 这就完了? 这么简单? 你这解析的是个啥啊. 合着ThreadLocal
的设计, 关键就是 JDK 开发者那有熟人啊.
K: 这么说其实也没错. ThreadLocal
的本质, 就是是一个以ThreadLocal
实例为key
,在Thread
上的ThreadLocalMap
里进行查找的过程.
-
threadLocal.get
相当于Map.get(threadLocal
-
threadLocal.set(value)
相当于Map.set(threadLocal,value)
拆穿了西洋镜, 天底下的魔术,都不复杂. 那么, 他就真的不复杂吗?
外部的部分不复杂, 甚至还可以通过修改Thread
类来优化掉.内部这Map
, 那是相等复杂.我估计这篇写不完, 就只挑一些能写明白的写写.
java.lang.ThreadLocal.ThreadLocalMap
这东西也是一个HashMap
, 他遵循一般的Hash表
技术原理.
-
根据 Hash函数
获取一个hashCode
-
用 hashCode
对内部数组大大小取模
. -
差别在于处理 hash冲突
的方式. 这里没有使用java.util.HashMap
里的拉链法
,而是使用了最普通的方式, `开放寻址法.
上次画HashMap
内部实现时候, 提到过, java.util.HashMap
使用了红黑树技术, 来给Key
的hashCode()
实现的比较烂的人兜底. 而这个地方作为Key
的ThreadLocal
的hashCode()
, 是开发者自己写的.会烂吗?
那肯定是不会啊. 所以,这里就用最质朴的开放寻址法
就够了. 相信,我的 hashCode 写的足够好, 所以, 我用不上拉链法. 嗯, 这是一种典型的,面向信仰编程.
我们作为凡人, 只需要围观, 这代码写的究竟有多好就行了.下面正式开始围观.
ThreadLocalMap 的初始化
-
初始化容量 INITIAL_CAPACITY = 16
-
扩容阈值 threshold = len * 2 / 3= 10
-
负载因子 没有, 或者说写死了 2/3
ThreadLocalMap 的增加
初始化最简单,肯定不会发生哈希冲突 这个就是取模的是快速写法, 没毛病.
firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
后面添加值时候, 可能就会冲突了.
你去住酒店, 电影一看你身份证号, 110 开头, 那你去住 110 号房吧. 到地方以后,发现 110 有人住了. 那就去 111,还有人那就去 112,以此类推.
for (Entry e = tab[i]; e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) { //这个U
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value); //这个是C
int sz = ++size;
这段代码看起来这么长, 其实就是循环查找第一个不为空的位置,然后塞进去. 这个就是开放寻址法.上面说的,挨门找就这意思.
不停的往后加一寻找, 到头了,就折返到 0. 这句倒也简单.
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
俗话说,Map4 要素, CRUD.
-
set 算是 C,U -
get 是 R -
remove 是 D
ThreadLocalMap 的删除
刚刚说了set
里的寻址. remove
里也是类似的, 取模,开放寻址,中规中矩. 除了这么一句怪怪的.
e.clear();
expungeStaleEntry(i);
clear 是把 entry 的 key 给清除掉了. 下面这句expungeStaleEntry
在做什么?
expungeStaleEntry
这段就非常有意思了.
刚刚用开放寻址法, 如果我们得到一个元素住在 110, 但是 110 有人了, 发生了哈希冲突, 会让他去住 111. 再来一个要住 110 的,会让他住 112.
如果 110 的客官退房了, 110 空出来了, 怎么办. 让刚刚被冲突碰走的客户都回来.把被删掉的元素和他后面第一个 null 之间的元素都给移动了.
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len))
这个 for 循环, 就是用来从staleSlot
下一个位置开始,循环处理接下来连续不为null
的位置.画个图的话, 是这样的.往后遍历 3 个位置.

移动元素的代码是这部分.
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;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
把前面的置空就置空, 不能置空的就往前移动.尽量凑个整.这串代码是不是有点眼熟? 这段代码的功能, 和前几天我们写过的ArrayList
里,batchRemove
有异曲同工之妙.
int h = k.threadLocalHashCode & (len - 1);
if (h != i)
这两句就是用来检查一个元素是不是被碰过来的. 如果在 112 发现了一个人,但是看证件他该住 110. 就是这个 if 做的检查.
tab[i] = null;
同志, 你的房间已经null
了,准备搬家吧.
新问题, 如果,这一趟, 110 退房,检查发现 111 是从 110 碰过来的,让他搬进去.然后 112 也被查出来是 110 碰过来的,怎么办.. 那就是这两句的作用.
while (tab[h] != null)
h = nextIndex(h, len);
112 就会被分配到 111 去了.
这块逻辑虽然短,代码也简单. 但是分析起来, 如果不先讲个故事, 真的很头疼.里面的复杂度分析, 更是让人抓狂. 这个利用开放寻址,线性探查法来做的删去, 被称为Algorithm R
.

喜欢的人可以去看那本书的 6.4 节,懂的都懂.
ThreadLocalMap 的查找
这个其实就没啥说的了本来. 我们前面, 很多步骤都用到了寻址, 找到再获取就行. 但是, 这个get
,他不止是查找. 还是回到我们故事里.
小王是个 110,到了酒店,被碰到了 112. 小张来到酒店找小王, 觉得他应该住 110, 去 110 一看不是小王. 按照正常的开放寻址,线性探查逻辑, 他该去 111 看看的.
可小张发现 110 空着, 他立刻呼叫酒店, 对店里顾客实施了一次刚刚说过的expungeStaleEntry
. 等客官们都搬完了, 再继续找下去.
CRUD 我们都说完了. 除了遗漏了一小点, set
方法中间有一段.
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
相信已经能猜出来这是在干嘛了. 替换废弃掉的 Entry. 这串写的更有趣, 会往前往后, 查找到 null,把两个 null 之间的废弃 Entry 全都清理掉. 另外, 这个里有一句话.A run is a sequence of entries between two null slots
, 这里可以概括成, 清理一个run
.
set 的结尾部分是这样的.
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
cleanSomeSlots 清理某些 slot,清理不成功并且 size 还是超过阈值了,rehash
.rehash 会调用resize
, 经典的 table 翻倍.这里不提了.
ThreadLocal 最复杂的部分, 其实不是整体的结构,结构部分很简单,面子够大的话谁都知道怎么写. 最复杂就是这个 Map 的部分, 我现在其实也弄不明白里面很多东西的复杂度.回头等我彻底弄明白了,再来重写.今天关于 Map 就先到这里.
ThreadLocal.hashCode()
敢在 ThreadLocalMap 里用开放寻址法, 说明作者们对 ThreadLocal 里的 hashCode 相当有信心.那么信心从哪里来, 这个 hashCode 到底哪里好.

这张图, 是在ThreadLocalMap
连续 50 个不同ThreadLocal
时的样子. 看不太清楚. 不过不要紧.能看出大致上, 都分布的比较散开, 比较严重聚集到一个地方不是太多. 每一个run
都不是很长.即使发生了上面的expungeStaleEntry
操作, 一般也只是局部的几个元素.
下面看算法.
hashCode 算法实现
public class ThreadLocal<T>{
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}
篇幅有限, 注释都被我去掉了. 也不难理解, 就是按照顺序, 第一个 ThreadLocal 是 0, 后面的依次增加 0x61c88647
. 通项公式应该是
.
Java1.2 版本的, 也是这个算法, 只不过那时候用的是synchronized
方法,现在换了CAS
版本.
这里有两个问题要注意
-
为什么 hashCode
是成倍增加的 -
为什么底数是 0x61c88647
第一个问题, 涉及到一个定理.

有了这个, 基本就可以保证, 上面的清理函数, 不会跑太久,复杂度降低.
第二个问题, 这个神奇的数字0x61c88647
就是ThreadLocal.hashCode
的核心. 那么这个数字是什么鬼? 看图.

随着 n 不断变大, 这个数字的二进制变化的样子. 好像还是有点看不清楚.我竖着排列一批看看.

因为我们的 table, 默认是 16 起, 翻倍扩容, 那么取模时候的,都是除以[16,32,64,128,256,1024,2048,4096,8192]. 我们把这个结果算出来.第一个是%16
的结果, 接着是%32
的,以此类推.

可以发现一件事, 这些数字在取模时候的特点.
-
同一行来看,在扩容了多次的情况下, 可以保证位置的稳定.有不少扩容了 5 次仍然稳定的 key. -
同一列来看, 极少有数字会相同.冲突概率很低.
那么, 这个神奇的数字 0x61c88647 到底是个什么东西? 又涉及到非常有趣的东西了. 当定理 S 中的 为 时候最一致分布.
这个技法叫斐波那契Hashing
. 如果计算机的 Integer 不是 32 位的, 那这里,其实就要换另一个数字了.
好了, 几天关于ThreadLocal
就研究到这里. 看不明白不要紧, 其实我也还没弄明白呢. 等我读两本书补补, 再来写写这东西.
未完待续.
原文始发于微信公众号(K字的研究):浅谈 ThreadLocal
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/24628.html