ThreadLocal详解
ThreadLocal有什么用
Synchronized的作用是同步线程使它们能安全地对共享变量进行操作,而ThreadLocal它的作用就是进行线程间的数据隔离,即每个线程都有自己的一个变量副本,彼此不会影响对方的数据。先来实践感受一下:
public class ThreadLoaclTest {
static ThreadLocal<Integer> tl = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
Runnable r = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
tl.set(i);
}
System.out.println(Thread.currentThread().getName() + " " + tl.get());
}
};
Thread t1 = new Thread(r, "t1");
Thread t2 = new Thread(r, "t2");
Thread t3 = new Thread(r, "t3");
t1.start();
t2.start();
t3.start();
}
}
t1 99
t2 99
t3 99
从结果中可见,虽然三个线程都是对同一个ThreadLocal对象进行操作,但是每个线程的值都没有被其他线程所影响。
首先解释一个误区
大家都知道ThreadLocal有个内部类ThreadLocalMap,其有个Entry节点类,ThreadLocalMap中是使用Entry节点来实现K、V映射的,但是在JDK1.8中,Entry节点的K并不是线程,而是ThreadLocal对象实例,V是这个ThreadLocal对象实例的值。并且这个ThreadLocalMap并不是ThreadLocal在维护,而是Thread在维护,即每个线程都有一个ThreadLocalMap。
JDK1.8改变了ThreadLocal的设计,有什么好处呢?
之前是ThreadLocal维护ThreadLocalMap,Thread作为K,那么有多少个ThreadLocal就有多少个ThreadLocalMap,而有多少个线程,ThreadLocal中就有多少个Entry节点。这样就造成了一个问题,一般情况下ThreadLocal的个数很少,而线程数很多。就会造成单个ThreadLocalMap很长,在获取元素时效率很低。
而JDK1.8之后决定ThreadLocalMap中元素个数的的ThreadLocal,所以不会很长,查询效率高,也没有因此而增大内存占用。
ThreadLocal的结构
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {//可以看到这里Entry节点弱引用了ThreadLocal
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
private static final int INITIAL_CAPACITY = 16;//初始容量
private Entry[] table;
private int size = 0;
private int threshold; // 扩容阈值
}
上面为简化之后的代码,可以看见ThreadLocal中有个内部类ThreadLocalMap,ThreadLocalMap中也有个内部类Entry,Entry弱引用了ThreadLocal,这样防止发生OOM。
赋值操作
set(T value)
public void set(T value) {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//获取当前线程维护的ThreadLocalMap
if (map != null)//如果当前线程维护的ThreadLocalMap已经初始化
map.set(this, value);//构造Entry节点加入ThreadLocalMap
else
createMap(t, value);//初始化并构造Entry节点加入ThreadLocalMap
}
getMap(Thread t) 和Thread中的threadLocals属性
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocal.ThreadLocalMap threadLocals = null;
可以看到getMap只是将当前线程的threadLocals属性返回,也印证了ThreadLocalMap是由Thread来维护的。
map.set(ThreadLocal<?> key, Object value)
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);//计算下标
//从下标位置开始遍历ThreadLocalMap
for (Entry e = tab[i];
e != null;
//这里更新了i,把i更新为i+1
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//找到ThreadLocal对应的Entry节点,做更新操作
if (k == key) {
e.value = value;//更新值
return;
}
//Entry不为空但K为空(即ThreadLocal为空,因为它是弱引用,所以可能被回收了)
if (k == null) {
replaceStaleEntry(key, value, i);//将节点移除
return;
}
}
//使用开放寻址法解决哈希冲突
//没有找到节点,添加节点,这里i已经被更新过了
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
createMap(Thread t, T firstValue)
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
调用这个方法初始化线程的ThreadLocalMap,并添加第一个节点。
查询操作
get()
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//同样获取当前线程的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//根据ThreadLocal获取节点
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;//获取这个节点的V
return result;
}
}
return setInitialValue();
}
getEntry(ThreadLocal<?> key)
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);
}
如何解决哈希冲突
计算下标的代码如下:
int i = key.threadLocalHashCode & (len-1);
&(len-1)这一部分大家想必十分熟悉,同HashMap一样是为了提高计算效率。
最主要的是:key.threadLocalHashCode
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);
}
可以看到threadLocalHashCode的值是通过AtomicInteger类的方法getAndAdd去在原始值上增加一个十六进制数来得到的。
第一个关键点:threadLocalHashCode是被fianl修饰的
所以每个ThreadLocal对象的threadLocalHashCode都是固定的
第二个关键点:nextHashCode是静态的
说明每个ThreadLocal对象都共享同一个nextHashCode,nextHashCode在nextHashCode()方法中被修改,不断再原值上增加HASH_INCREMENT,而nextHashCode()方法只会被调用一次,就是当对象实例化的时候。这样就实现了不同的ThreadLocal对象的threadLocalHashCode基本不会相同。
我们通过一个例子来说明一下:
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
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);
}
public static void main(String[] args) {
Main a = new Main();
Main b = new Main();
Main c = new Main();
for (int i = 0;i<2;i++){
System.out.println("第"+(i+1)+"次打印");
System.out.println(a.threadLocalHashCode);
System.out.println(b.threadLocalHashCode);
System.out.println(c.threadLocalHashCode);
}
}
}
第1次打印
0
1640531527
-1013904242
第2次打印
0
1640531527
-1013904242
可以看到,我们根据ThreadLocal的样子构建了一个Main类(下面我们把它当作ThreadLocal类),在main方法中新建三个ThreadLocal对象,打印这三个ThreadLocal对象的threadLocalHashCode,每个都不一样,但是每次打印三个的值都不会变。
因为我们在new第一个ThreadLocal对象实例a的时候,就通过nextHashCode()方法给它的threadLocalHashCode赋了初始值,因为这个时候nextHashCode第一次使用,所以为0;创建第二个ThreadLocal实例b时,同样的流程,但这时候因为a曾经调用过nextHashCode()方法把nextHashCode更新了(因为nextHashCode.getAndAdd(HASH_INCREMENT)),所以b的threadLocalHashCode也就变了;创建c的时候同理。
至于为什么要nextHashCode.getAndAdd(HASH_INCREMENT)给原值加上HASH_INCREMENT这个值,那是因为不断加这个值形成的队列类似斐波那契数列,十分契合2^n数组。可以看一下下面的演示:
for (int i = 0;i<16;i++){
System.out.print((nextHashCode()&15)+" ");
}
5 12 3 10 1 8 15 6 13 4 11 2 9 0 7 14
可以看到假设ThreadLocalMap的容量是16,那么拿nextHashCode()&(16-1)计算出来的16个下标竟然没有重复。可见对于2^n容量的数组,这个算法计算出来的下标出现哈希冲突的概率很小很小。
总结
ThreadLocal依靠在Thread中维护的ThreadLocalMap(初始容量n=16,扩容阈值为n*2/3)来实现数据隔离,以ThreadLocal对象为K,以要存的值为V构建Entry节点,然后通过ThreadLocal的哈希值来计算在ThreadLocalMap中的存储下标,将Entry节点存进去。这样每个线程都有自己的一份ThreadLocal和值的副本,实现数据隔离。要获取值时,就通过ThreadLocal去ThreadLocalMap里面找,也是获取它的哈希值然后计算下标并找到Entry,假如Entry的K为空,说明弱引用的ThreadLocal被回收了,就从ThreadLocalMap中删除,否则返回这个Entry的V。