并发编程之ThreadLocal
在多线程开发过程中,我们往往不能忽略并发安全
的问题。处理线程安全的问题有很多,例如让变量不可变
,使用线程安全的集合
,使用JUC包下的相关类(如Atomic包)
等等,这里的话我们简单总结一下ThreadLocal这个常用的类。
解决的问题
首先来看一些很常见的场景,当我们开发一个Web网站。不同的用户进来能显示不同的信息,并且各自的用户信息都能存在各自的Session中。
第二,如果我们想在某个方法的下游
获取一些信息,但是又不想通过传参的方式将数据带下去
。
针对以上两种情况,我们可以使用ThreadLocal来解决。
引入ThreadLocal
ThreadLocal音译是线程本地,其实本质是线程独享
。
也就是说,每个线程都各自拥有,某一个线程的线程并不会影响另外一个线程的值。
我们来看一段示例代码:
public class TestThreadLocal {
static ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 第一次赋值
stringThreadLocal.set("我是主线程");
// 开启一个子线程修改值
new Thread(()->{
// 第二次赋值
stringThreadLocal.set("我是子线程");
System.out.println(Thread.currentThread().getName()+" : "+ stringThreadLocal.get());
}).start();
try {
// 休眠一秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" : "+stringThreadLocal.get());
// 第三次赋值
stringThreadLocal.set("我是修改后的");
System.out.println(Thread.currentThread().getName()+" : "+stringThreadLocal.get());
}
}
来看结果:
为了确保子线程能成功对ThreadLocal变量进行操作,特地休眠了一秒让子线程执行完。(也可以用join来插队)
我们可以看出,子线程对ThreadLocal变量的操作丝毫不会影响主线程,主线程是怎么赋值的那么在读取的时候还是什么。
并且,如果我们在主线程修改的话,再次获取到的值就是主线程修改的值。
那么接下来,我们就要从其数据结构来研究ThreadLocal是如何做到对线程的控制的。
数据结构
首先,我们通过一张图来看ThreadLocal和线程的关系:
从下图我们可以看到,每一个线程维护着一个ThreadLocalMap
,然后ThreadLocalMap中维护着多组以ThreadLocal为键的节点
下面,我们从源码层面来看结构:
既然是线程独有的
,可以从Thread
类中找到这个threadLocals
变量,发现他的类型就是ThreadLocalMap,不过是由ThreadLocal类来维护的
我们去ThreadLocal中找到这个内部类:
发现这个类内部维护了一个Entry节点
和tables数组
,看到这里几乎可以确定,他跟HashMap结构类似了。
但是,HashMap解决冲突时拉链式法
,凡是Hash冲突并且值不相等的就以链表接下去,形成一条链。而ThreadLocalMap则是线性探测法
,凡是Hash冲突,就让后者往下找数组空位插入即可。
并且,这个Entry类继承WeakReference这个弱应用
,通过构造方法看到他的健是一个弱引用
(这里要稍微了解一下JVM的强软弱虚引用在什么时候会被垃圾回收)。也就是这一步导致内存泄漏的。
到这里,基本也可以看出了。
每个线程各自维护一个Map
,这个Map内部数组存放Entry
节点,而Entry以ThreadLocal
为键,Object对象为值。
接下来,通过了解他的相关方法,可以更加明白他的机制。
相关常用方法
以下是一些常用的方法
getMap
我们先来看getMap这个方法,通过传入一个线程,返回这个线程对应的threadlocalmap。
为什么要先讲这个方法呢,因为这个方法是后面所有方法的爹(详解看后文)。
set方法
接下来,我们来到set方法。他先获取当前线程,在通过getMap方法来获取到线程对应的map进行插入操作。
map存在就正常插入,不存在则创建。
后面具体的方法跟HashMap大同小异,除了Hash冲突的解决。这里就不过多解释了。
get方法
也是先通过getMap方法传入当前线程获取map
如果map不为空,则通过调用get方法的threalocal来获取存在当前线程map的值
如果为空,则通过setInitialValue来初始化这个map
initialValue方法
initialValue默认返回空值,可以进行重写
。
如果当前threadlocal没有进行set操作就get就会触发initialValue;如果threadlocal先进行set操作在get,那么这个initialValue不会执行,除非清空了所有元素重来。
remove方法
移除该thredlocal在此线程map的节点
空指针和内存泄漏
内存泄漏
简单介绍完ThreadLocal常用的方法后,下面说一下他的问题
。
前面也有提到过,ThreadLocalMap中的节点是弱引用
,也就意味着,下一次GC的时候就把这个threadlocal键给回收了。此时,因为线程还存在,所以map还存在,map还存在Entry节点就存在,那么键的引用丢失,我们找不到这个值,但是值是强引用,所以存在内存泄漏
下面通过GC前后的对象引用状态来看:
GC后键被回收了
,而value只能根据键来获取
,所以最后带来了内存泄漏。
空指针问题
这个的话一般不会带来问题,只不过要注意的是如果要对threadlocal进行封装处理,要注意自己封装get的返回值
示例代码:
public static long get(){
return longThreadLocal.get();
}
public static void set(long l){
longThreadLocal.set(l);
}
public static void main(String[] args) {
//set(1L);
System.out.println(get());
}
执行结果:
我们可以看到,出现了空指针异常。
因为本来用ThreadLocal传入的泛型就是对象类型或基本类型的包装类,正常获取的话要么是NULL,要么是set进去的值
。这里进行了一层封装,返回基本类型,但是我们把set方法注释掉了,就造成了装箱拆箱的时候报出空指针异常。
应用
ThreadLocal在Spring的上下文中有内用。根据前面存入的信息,可以在后层service服务层通过上下文来获取存入的信息。
小结
最后,对ThreadLocal简单小结。
首先,结构类似Map,处理Hash冲突使用线性探测法
;
其次,能保证数据的安全,因为线程独享,互不干扰
;
问题,带来内存泄漏和空指针问题,内存泄漏是因为键的弱引用
;空指针则是基本类型的装箱拆箱
。
最后,如果不需要用ThreadLocal尽量不用,因为也会带来一些不必要的性能损耗。