一、前言
最近项目中,有个同事开发中定义了一个全局变量,为了防止数据混乱【并发请求这个接口时不同线程操作,会影响变量值】,他加了一个锁,这就大大影响了性能。我默默推荐了ThreadLocal,说可以解决他的问题。最后他虽然用了ThreadLocal,但又遇到问题,说代码执行到后面发现值莫名被置空了【看了代码发现,为了不影响返回结果,异步执行了一部分业务】。。。。。
基于项目中使用到了ThreadLocal,同时一些复杂的场景,所以这篇文章将介绍ThreadLocal系列的原理,以便更好的使用。
使用ThreadLocal常见有以下的问题要思考:
(1)、主线程怎么传值给子线程
(2)、子线程修改了变量值,对主线程或其他线程是否有影响
(3)、ThreadLocal这么好用,是否无节制的使用,有什么需要注意的么?
因此,这篇文章将介绍ThreadLocal、InheritableThreadLocal(ITL)、TransmittableThreadLocal(TTL)的原理和优缺点。
二、ThreadLocal
ThreadLocal是避免多线程并发问题,避免竞争。主要是set()、get()、remove()几个方法。
2.1、为什么会用到ThreadLocal(ThreadLocal应用场景)
对时间的转化,我们经常使用SimpleDateFormat 类完成。常见使用方法:
方法一
public static String formatDate(Date date)throws ParseException{
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
public static Date parse(String strDate) throws ParseException{
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(strDate);
}
------将创建对象变成私有,解决多线程问题,但加重了创建对象的负担。
方法二
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String formatDate(Date date)throws ParseException{
return sdf.format(date);
}
public static Date parse(String strDate) throws ParseException{
return sdf.parse(date);
}
------new SimpleDateFormat对象虽然变成全局变量,但在多线程下会出现日期转化报错、日期转化混乱问题。
问题原因:SimpleDateFormat是线程不安全的。在parse()方法传入的类成员变量,在多线程下Calendar对象的方法会造成数据不安全的问题。
最佳考虑方法,在多线程下,是否可以为每个线程分配对应的资源,防止出现竞争错误,ThreadLocal很好的解决了多线程并发问题。
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
protected DateFormat initialValue(){
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static String currentDate() {
return threadLocal.get().format(now());
}
private static ThreadLocal<DateFormat> threadLocal2 = new ThreadLocal<DateFormat>();
public static DateFormat getDateFormat(){
DateFormat df = threadLocal2.get();
if(df ==null){
df = new SimpleDateFormat(DT_FORMAT);
threadLocal2.set(df);
}
return df;
}
public static String currentDate() {
return getDateFormat().format(now());
}
2.2、ThreadLocal实现原理
目的:保证当前线程中有自己的变量,不会出现多线程并发安全问题。
实现原理:
Thread为每个线程维护唯一的ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,用Entry数组存储数据【将存储这个线程的所有ThreadLocal实例】;
Entry是继承弱引用,使用K-V方式组织数据,其中K是当前线程的不同ThreadLocal对象实例,V是存储的对象值;
主要方法是set、get、remove,都是先currentThread()获取自身线程,然后对ThreadLocalMap进行操作。不同线程拥有自身的ThreadLocalMap,因此实现“数据隔离”。
具体的对照Thread、ThreadLocal类的关系和下面的图表示
Thread{
ThreadLocal.ThreadLocalMap threadLocals = null;
}
ThreadLocal{
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
//K-V方法存储数据。K是这个Thread线程的某个ThreadLocal的实例,V是对应数值
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
2.3、ThreadLocalMap
ThreadLocalMap是内部类,利用Entry数组存储数据,Entry是继承弱引用。
Entry使用K-V方式组织数据。
ThreadLocalMap的生命周期跟Thread(注意线程池中的Thread)一样长,如果没有手动删除对应key(线程使用结束归还给线程池了,其中的KV不再被使用但又不会GC回收,可认为是内存泄漏),一定会导致内存泄漏。
但是使用弱引用可以多一层保障:弱引用ThreadLocal会被GC回收,不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除【但没下一个的话,就不能清除】。
解决hash冲突—【hreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1及线性探测,寻找下一个相邻的位置。】
(1)、ThreadLocalMap如何解决hash冲突
ThreadLocalMap使用 线性探测的开发地址法(线性探测法) 解决hash冲突。当key存在hash冲突,会线性的往后探测直到找到为null的位置存入对象,或者找到key相同的位置覆盖更新原来数据。另外,在这过程中,如果发现Entry不为空但key为null的位置,会启动探测式清理法。
2.4、为什么Entry的K使用弱引用?
作用:为了处理非常大和生命周期非常长的线程,哈希表使用弱引用作为 key。
现有引用链条是:Thread—ThreadLcoalMap—Entry—k(弱引用)–V(强引用)
如果K是强引用:一个线程中,当ThreadLocal对象实例被用完,准备回收时,ThreadLcoalMap是拥有这个ThreadLocal的引用,那么这时候ThreadLocal是不能被回收,会造成K-V会一直在ThreadLcoalMap中存在,发生内存泄漏;
如果K是弱引用:一个线程中,当ThreadLocal对象实例被用完,准备回收时,由于K使用弱引用,当没其他对象的强关联引用下,ThreadLocal对象被回收,那么对于V来说【按照Java8特性】,在下一次ThreadLcoalMap调用set、get方法时会清空Entry的K为null的信息。
2.5、为什么Entry的V不设置为弱引用?
如果V是弱引用,它没被其他对象强关联,那么会被GC回收,这时候线程获取value时得到null结果。
V是我们真正想保存的数据,这个值不能无端被null,所以不能被弱引用。
2.6、ThreadLocal中内存泄漏问题(为什么会有内存泄漏)?
由于ThreadLoca