ThreadLocal在项目中使用到的情况比较少,只知道可以做到线程隔离。如果某个对象可能产生并发错误,那么常规解决方案就是对其加锁,另一种思路就是使用ThreadLocal保证线程隔离。
首先查看ThreadLocal的get方法:
public T get() {
//当前线程
Thread t = Thread.currentThread();
//当前线程持有的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//得到ThreadLocalMap中键为当前ThreadLocal的条目
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
简单的理解就是,一个线程和一个ThreadLocal对应了一个唯一的对象。
继续深入查看下去,getMap返回的就是线程对象持有的ThreadLocalMap,这个类虽然没有继承Map接口,但是现在可以把它当作map来看待,它的键值是ThreadLocal类。当这个map不为空时,就从它取出来数据,当它为空时,调用setInitialValue方法。
private T setInitialValue() {
//initialValue默认返回null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
//设置默认的初始值
map.set(this, value);
else
//创建ThreadLocalMap插入默认初始值并设置给当前线程t
createMap(t, value);
return value;
}
ThreadLocalMap
之前说了ThreadLocalMap并不是Map,它是ThreadLocal的内部静态类,内部存储的数据结构是一个Entry数组。Entry继承自WeakReference<ThreadLocal<?>>,并持有一个value对象。
现在的问题就是ThreadLocalMap是怎么通过一个ThreadLocal对象来找到对应的Entry数组下标?
重新查看createMap方法,调用了ThreadLocalMap的一个构造方法。ThreadLocalMap共有两个构造方法,另一个private的构造方法是为了InheritableThreadLocal设计的,此处不介绍。查看源码:
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化Entry数组,大小为16
table = new Entry[INITIAL_CAPACITY];
//计算得到下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//创建条目放入该下标位置
table[i] = new Entry(firstKey, firstValue);
size = 1;
//设置临界值为数组长度的2/3
setThreshold(INITIAL_CAPACITY);
}
介绍一下这个计算的方法,threadLocalHashCode是每个ThreadLocal对象初始化时得到的一个值,这个值是0x61c88647的倍数。INITIAL_CAPACITY是数组的长度,通过保证数组长度永远是2的n次方,从而保证其减去一得到的二进制是一串全部为1长度为n的数字,与其做&操作就可以理解为取二进制最后n位。而0x61c88647,与fibonacci hashing(斐波那契散列法)以及黄金分割有关,特殊的哈希码0x61c88647大大降低碰撞的几率,能让哈希码能均匀的分布在2的N次方的数组里。
long l = (long) ((1L << 31) * (Math.sqrt(5) - 1)); //Math.sqrt(5) - 1 = 1.2360679774997898
System.out.println("as 32 bit unsigned: " + l); //2654435769
int i = (int) l;
System.out.println("as 32 bit signed: " + i); //-1640531527 = -0x61c88647
查看ThreadLocalMap的getEntry方法,发现就是通过该计算方式来得到数组下标的:
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//条目非空且弱引用的ThreadLocal等于入参
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}