前言
ThreadLocal本身不存数据, 在某个线程调用ThreadLocal的set方法保存对象时,会在当前线程创建一个ThreadLocalMap将ThreadLocal作为Entry的一部分存放进去。线程才是最终保存数据的地方。
一个线程可以绑定多个ThreadLocal, 在ThreadLocalMap内部, 是采用数组的方式来存放,每次存放和读取,都要根据当前threadLocal去遍历数组来找到对应的entry, 再去操作对应的value。

如果将Thread比作学生的话, ThreadLocal就是学科名, 一个学生可以有多个学科的成绩单,都放在自己身上,针对同一个学科, 不同的学生有不同的成绩单, 相互不影响。 你只需要告诉我一个学科名,我就能拿到对应的成绩单。
代码解读
ThreadLocal有两个方法: set/get, 先看下set方法
java
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
//设置值, this指的是当前的threadLocal对象
map.set(this, value);
else
//如果当前线程没有threadLocalMap, 进行创建
createMap(t, value);
}
//getMap操作只是获取当前线程的threadLocals对象
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//createMap只是简单的创建一个ThreadLocalMap对象
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
再来看看get操作:
java
public T get() {
Thread t = Thread.currentThread();
//获取当前线程的 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//获取当前threadLocal的数据副本
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
//返回最终需要的值
T result = (T)e.value;
return result;
}
}
//get不到, 可以设置默认值并获取默认值
return setInitialValue();
}
ThreadLocalMap解读
ThreadLocalMap是ThreadLocal的静态内部类, 最核心复杂的代码就在这个方法里面。
java
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//threadLocalHashCode是在threadLocal创建时候就定义好的, 多个threadLocal的这个值会不一样
//这是一个神奇的数字, 其目的,是为了保证能key均匀分布, 下面会给例子
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
//nextIndex表示当前下标有人了, 取下个id
e = tab[i = nextIndex(i, len)]) {
//key相同, 直接设置值
if (e.refersTo(key)) {
e.value = value;
return;
}
//entry已经被回收了, 占用这个entry
if (e.refersTo(null)) {
replaceStaleEntry(key, value, i);
return;
}
}
//设置新值
tab[i] = new Entry(key, value);
int sz = ++size;
//扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
java
//获取下一个下标, 在不超过数组长度时,直接加1
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
下面是get操作代码:
java
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//大部分thread只会绑定一个threadLocal, 所以先判断是否第一个就是, 是的话直接返回
if (e != null && e.refersTo(key))
return e;
else
//很不幸,第一个不是, 要搜下数组
return getEntryAfterMiss(key, i, e);
}
java
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
if (e.refersTo(key))
return e;
//过期清理
if (e.refersTo(null))
expungeStaleEntry(i);
else
//真的是一个一个搜
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
神奇的0x61c88647
ThreadLocalMap使用开放地址法(open addressing) 来解决hash冲突,其做法就是取一个初始值,对数组长度进行取余,拿到对应的下标。然后把数据存放到数据下标的位置即可。存放下个key时,再加一次初始值,对数组长度进行取余,拿下标。
数据分布是否均匀,全看这个初始值取得好不好。ThreadLocal使用了一个0x61c88647来进行初始值计算。可以看下代码:
java
//初始值是0
private static AtomicInteger nextHashCode =new AtomicInteger();
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
//每次递增 0x61c88647
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
java
public class ThreadLocal<T> {
//每个ThreadLocal对象, 创建时立即获取一个hashCode
private final int threadLocalHashCode = nextHashCode();
private Entry getEntry(ThreadLocal<?> key) {
//计算下标, hashCode和数组长度进行取余操作
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.refersTo(key))
return e;
else
return getEntryAfterMiss(key, i, e);
}
先测试下这个数字是否真得有用。
java
public class MagicNumberTest {
private static final AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
public static void main(String[] args) {
int[] lenArray = new int[]{16, 32, 64, 128, 256, 512, 1024, 2048, 4096};
for (int len : lenArray) {
//构造一个16,32,64位长度的数组
boolean[] pool = new boolean[len];
for (int i = 0; i < len; i++) {
//ThreadLocal在创建的时候, 获取一个hashCode
int hashCode = nextHashCode();
//对数组长度取余, 获取存储下标
int index = hashCode & (len - 1);
if (pool[index]) {
//下标冲突(使用这个魔数, 会均匀填满整个数组, 不会冲突)
System.out.println("duplicate: " + len + ", index:" + index);
} else {
pool[index] = true;
}
}
}
System.out.println("finished");
}
}
日志只打印了一个finished, 也可以尝试更改魔数, 可以证明这个数字真得有神奇的魔力。它就是能让数字均匀分布在整个数组上(偶数倍数组长度)
为什么是0x61c88647
首先复习一下斐波那契数列:
- 斐波那契数列: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
- 通项公式: 假设F(n)为该数列的第n项, 那么这句话可以写成如下形式: F(n) = F(n-1) + F(n-2)
当n趋向于无穷大时, 前一项与后一项的比值越来越逼近0.618... , 而这个值0.618就被称为黄金分割数。证明过程如下:

黄金分割数的准确值是(根号5-1)/2, 约等于0.618

0x61c88647 转成10进制是 1640531527, 而32位带符号整数的黄金分割值是 -1640531527, 实际上用 -1640531527测试也是可以完美分布的。
-1640531527 通过不断的追加自身,也能得到1640531527, 所以猜测是为了使用习惯,才用了 1640531527。
只能说黄金比例很神奇!
ThreadLocal里面的Entry为什么是弱引用
弱引用的定义:
对象可以同时有弱引用和强引用。
只有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程一旦发现了只有弱引用的对象, 不管当前内容空间是否充裕,都会回收它占用的内存。
不过,由于垃圾回收期是一个优先级很低的线程, 因此不一定会很快发现只有弱引用的对象
java
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
回归本质, ThreadLocalMap是用来存放对象的,虽然是存放在thread中, 但是存放和提取的钥匙却是ThreadLocal对象。 如果ThreadLocal对象无法触达, 那么它在所有线程中关联的变量都无法进行访问(正常访问路径), 所以理论上应该被释放掉。
现在的问题是,ThreadLocal还在被Thread(可能多个)对象里面的threadLocals对象引用, 所以按照正常的流程, 它无法被回收, 除非主动调用了remove操作。
将ThreadLocal里面的Entry设置为弱引用, 当ThreadLocal没有强引用的时候, 就可以直接被回收。也就是Thread里面的ThreadLocalMap对Entry里面对象的引用是弱引用。
注意: WeakReference引用本身是强引用, 它内部的(T reference) 才是真正的弱引用字段, WeakReference就是一个装弱引用的容器而已。
key设置为弱引用有什么用

设置弱引用只是针对ThreadLocal来设置,也就是Entry对象里面的key, 当ThreadLocal不再有强引用时, 则会进行回收, 结果就是Entry里面的key为null了。 如果只有这一种措施, 那么Entry不会被回收, 任然有可能内存溢出。
ThreadLocal在进行set操作的时候, 如果检测到地址冲突, 会检查下key是否为存在, 如果不存在则进行替换,这样才算避免了内存溢出。
java
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) {//作为key的ThreadLocal对象被回收了, 此Entry将会被替换
replaceStaleEntry(key, value, i);
return;
}
}
为什么用开放地址法
- ThreadLocal本身数据量就不会很大,使用开放地址法可以节约空间
- hash能够均匀分布, 效率本来就高
总结
- ThreadLocal本身不存对象, 只是提供了一个key和操作方法入口
- Thread里有个threadLocals字段, 类型是ThreadLocal.ThreadLocalMap, 它用来存放数据
- 比较意外的是, ThreadLocalMap是用数组来存放详细数据的, 存放的时候, 通过遍历下标来找到合适的index, 读取的时候,先读取最新的一个,没找到再依次去找
- InheritableThreadLocal也是一个ThreadLocal, 操作方法类似, 在Thread对象中,也有单独的存放字段: inheritableThreadLocals
参考文章
hash 结构的另一种形式 ------ 开放地址法 - 掘金