关于ThreadLocal平时用的真的不多,目前做了有7,8个项目,都没有使用过ThreadLocal,所以对ThreadLocal还是比较陌生。对它的唯一认识是从Handler的源码中,也知道ThreadLocal是做线程数据隔离的,其他的就一概不知了。最近打算好好看看ThreadLocal源码。发现了好多新的知识,对ThreadLocal也有了更深的认识。而ThreadLocal在多线程中也有着比较重要的地位。这篇博客我会从使用,源码,问题这三个方面进行分析
一、ThreadLocal的使用
我相信不太了解ThreadLocal的同学,至少也知道ThreadLocal可以做线程间数据的隔离,线程之间的数据互不影响。而ThreadLocal对外开放的方法主要就是
我们来看一下它的使用
public class Main2 {
public static void main(String[] args) {
ThreadLocal<String>threadLocal = new ThreadLocal<>();
//存入数据
threadLocal.set("gzc");
//取出数据
System.out.println("result:"+threadLocal.get());
//移除数据
threadLocal.remove();
}
}
有的小伙伴会说,你这个写的是什么呀?也没有体现出来线程间的数据隔离啊!是的,上面只是三个普通的方法的调用。我们继续看下面的代码:
public class Main2 {
public static void main(String[] args) {
ThreadLocal<String>threadLocal = new ThreadLocal<>();
threadLocal.set("gzc");
new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("sub_gzc");
System.out.println(Thread.currentThread().getName()+" result:"+threadLocal.get());
threadLocal.remove();
}
},"sub_thread").start();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" result:"+threadLocal.get());
threadLocal.remove();
}
}
首先我们依然在主线程中给ThreadLocal对象进行了赋值,之后开启一个子线程,使用同样的ThreadLocal对象进行了赋值,获取,移除。我们在主线程中延迟了一段时间,保证子线程先执行。
之后再在主线程中打印threadLocal的值。我们看结果
sub_thread result:sub_gzc
main result:gzc
从结果中我们可以看出来,子线程先执行并且子线程中的threadLocal的操作并不会影响主线程中的threadLocal中的值。所以我们平时如果用到ThreadLocal的话,可以把ThreadLocal定义为static,这样就可以全局使用了。而阿里规范中也提到过:
ThreadLocal无法解决共享对象的更新问题,ThreadLocal对象建议使用static修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配了一块存储空间,所以此类的对象(只要在这个线程内定义的)都可以操控这个变量。
什么叫做无法解决共享对象的更新问题?每个线程向ThreadLocal中读写数据是线程隔离,互相之间不会影响的。
在使用的时候,为什么要进行remove,其实是为了防止内存溢出的问题,我之后会说这个地方。
如果在一个线程中使用了多个ThreadLocal会怎么样?同学可以自行测试一下,多个ThreadLocal所保存的结果互不影响。如果我们在多线程的情况下使用ThreadLocal,这个情况会出现很多,毕竟,我们要保存的数据不是只有一个,可能是多个。所以会有多个ThreadLocal,这种情况很正常。
关于ThreadLocal的使用,其实也就是这么多了。下面,我们从源码中来看ThreadLocal
二、ThreadLocal的源码
我们如果进入ThreadLocal的源码中,我们可以发现不论是set还是get和ThreadLocal的内部类ThreadLocalMap息息相关,我们以set方法为例:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
我们可以看到,在进行值的存储时,就是以ThreadLocal为key。这么看来,ThreadLocalMap和我们使用的Map很像。我们进入这个getMap中看看:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
每个Thread类中,都保存着一个ThreadLocalMap
我们进入ThreadLocalMap中,可以看到几个比较关键的地方:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
......
}
内部类Entry:Entry是key和value的载体,不过Entry是一个弱引用。为什么要使用弱引用呢?请看这篇文章
table:所有的Entry在ThreadLocalMap中都是以数组的形式存储的。
看到这里,估计小伙伴会有很多的问号。为什么要用数组?如果用数组,怎么准确的把Entry取出来?我们一个一个来说清楚
怎么准确的把Entry取出来?
ThreadLocal中有如下几个属性和方法:
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);
}
我们可以看见,nextHashCode是AtomicInteger(关于AotmicInteger,小伙伴自行百度~),并且是静态的,所有ThreadLocal实例,共享使用一个nextHashCode。每次ThreadLocal被创建的时候,threadLocalHashCode都会被赋值,而这个值都是通过nextHashCode的getAndAdd方法获取,也就是把这次传入的值和上次的值相加。而这里的值是HASH_INCREMENT,也就是说每次threadLocalHashCode都是上一个threadLocalHashCode加上HASH_INCREMENT。为什么HASH_INCREMENT是0x61c88647,建议小伙伴看【JAVA并发编程系列】ThreadLocal 这篇文章,反正我是没懂,但是又感觉有道理....
ThreadLocal在进行值存储的时候,会调用ThreadLocalMap中的set方法,其中有这么一句代码:
int i = key.threadLocalHashCode & (len-1);
这里的i就是table数组的索引,threadLocalHashCode和(len-1)进行了&操作,得出的值一定小于登录len-1,也就是说索引一定在table数组大小范围内。不同的ThreadLocal会不会计算出相同的索引呢?肯定会的。ThreadLocalMap在进行set的时候,先判断此位置是否被占用了或者占用此位置的对象变成了空,如果是,则直接插入;如果不是,得到的索引值就会+1,重新检索添加。取值是一样的道理,如果取到的值不为空并且key相等,那么取出相应的值;否则进行+1重新检索。
为什么要用数组呢?
数组的内存地址可以进行复用,并且是连续的,所以迭代的时候效率更高。因为在ThreadLocalMap中进行了多次清除无用对象的操作,需要进行多次迭代,所以呢使用数组更加合适。
ThreadLocal和ThreadLocalMap还有其他的源码,但是个人认为,上面的代码涉及了一些设计思想,所以更加重要一些。其他的代码更多的是细节的操作,有兴趣的同学可以继续阅读源码或者看参考文章。
三、内存泄露问题
ThreadLocal其实存在着内存泄露的问题,Entry虽然本身是弱引用,但是内部存储的value确是一个强引用。如果ThreaLocal变为了null,JVM扫描到了,就会把Entry回收,但是value这个强引用确回收不了。所以在ThreadLocalMap中有着大量的清除无效Entry的方法。这样做会存在一个问题:Thread的生命周期很长,并不会随时调用清除的方法。依然会存在内存泄露的问题?那我们应该怎么办呢?我们是使用完ThreadLocal之后,调用ThreadLocal中的remove方法进行手动清除即可。贴出一张图,供大家参考:
参考博客
面试官:知道ThreadLocal嘛?谈谈你对它的理解?(基于jdk1.8)