前言:借鉴左耳耗子的话术,学习每一样技术都必须知道这项技术本身解决的问题是什么?
问题:
- 在处理线程安全问题的时候,我们最常用的方式就是加锁来控制,但是加锁的方式往往会使某一个时间片只会有一个线程拿到锁去处理业务,在整体来都会因加锁而损失一定的性能,有没有一种可以彻底避免竞争的方式呢?
- 在常规的业务开发过程中,比如登录信息,可能在后续整个链路方法内都需要用到,虽然采用方法形参传递是可以解决,但是这样是不是增加了维护的成本呢?在多开发同学接手后可能改的面目全非,有没有一种方式可以解耦呢?
答案是有的,那就是ThreadLocal
针对第一个问题,TheadLocal是利用空间换时间的概念,从根本上杜绝了变量竞争,每个线程各自独享自己的一份内容;那么第二个问题也是可以游刃有余的解决,在首个过滤器把登录信息存储到这个ThreadLocal后,后续哪个方法需要用到,就自己通过ThreadLocal拿出来即可。
ThreadLocal的使用:
这里用String代替要存储的对象,实际上根据业务调整;
public class ThreadLocalUtil{
//申明一个静态的ThreadLocal对象
private static ThreadLocal<String> tlHolder = new ThreadLocal<>();
public static set(){
tlHolder.set("kobeBtryant");
}
public static get(){
return tlHolder.get();
}
public static clear(){
tlHolder.remove();
}
}
一般用法比较简单,这里就不介绍,直接上关系图:
从ThreadLocal的set方法看出,ThreadLocalMap 是 Thread的一个成员变量,每个线程里面保存的一个map,key是对应相关的ThreadLocal对象,value是不同线程需要设置的一个值。
以下是Thread的成员变量,第一个Map就是记录当前线程的使用的所有ThreadLocal变量,顺便提下第二个Map inheritableThreadLocals,这是存储父线程的ThreadLocal的变量(后面会提到,这里先过)
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
再来看看ThreadMap结构:
Entry继承的是WeakReference对象,当new ThreadLocalMap的时候会传递当前ThreadLocal对象进去,而这个key最终会被调用Entry构造方式里面super(k);也就是会加入到弱引用的队列当中,这里的设计是为了避免内存泄漏,具体我们可以看下面的面试环节(难受)=。=
灵魂拷问:为什么ThreadLocalMap里面的key是弱引用?
1.我们假设一下,如果在业务过程中,ThreadLocal instance不再被任何一个线程引用的时候,如果key是强引用,虽然引用key的线程对象已经销毁,但是key还强引用指向一个instance,那么就有可能造成内存泄漏。James Gosling考虑到这个问题,这里应该设置成弱引用,当instance不会被引用时,就可以自动回收这个对象,就避免了内存泄漏的问题了。
2.回归到ThreadLocalMap本身,key是解决了,value还是强引用呢,key 回收了,value还在强引用呢,正常情况下,如果线程被回收,而且value没有被其他对象引用,那么value也就该回收了,但是这个伴随线程存在的问题,如果线程周期比较长,那么最终还是会出现内存泄漏。
但是,假设线程一直没结束(原因很多,比如死锁等待,或者业务执行很长),那么key都没有被回收,那么这部分也将会一直回收不了,这里JDK做了一个优化,我们可以看到对应的ThreadLocal的set方法、remove方法、rehash方法,都会扫描key 为null的entry,当发现为null时,对应的value也会被指向null(这里采用的是开放寻址法,有兴趣可以自行查看源码),从而可以回收。
尽管如此,假设我们的业务场景一直访问高频的ThreadLocal对象,那么低频的可能永远不会访问,那么JDK是无法洞察我们想干什么,只能留在JVM里面,所以尽可能养成良好的习惯,当不需要使用ThreadLocal对象的时候,应该主动调用remove方法,避免内存泄漏。
总结:以下不能通过被动措施保证不了内存不泄漏
1.static修饰的 ThreadLocal,延长了生命周期,如果没有remove 会存在泄漏
2.分配了使用的ThreadLocal,不再调用get、set、remove、value会一直泄漏
针对用的最多的SpringMvc拦截器来说,我们一般会做很多拦截器去做一些业务处理,这里线程是复用的,假设一段时间内没有任何请求,那么上一次请求没有remove,那么上一次的通过ThreadLocal存储的对象就会保留在ThreadLocalMap中,我们又假设N个线程 * M个大对象 * Y个ThreadLocal,那么就会有一部分占用着内存,而又不会被用到,最后导致OOM
【加餐部分】
这个背景也是原于我们正常业务处理中,往往会用到多线程,通过子线程去完整某些任务,但是由于ThreadLocal本身是线程安全的,那么有什么办法让子线程也能看到父线程的本地变量呢? 答案是上文提到Thread里面会存另外一个变量inheritableThreadLocals;
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
//设置在线程的inheritableThreadLocals变量中
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
这就是父线程传递内容到子线程的秘密,通过结合ThreadLocal的get()方法查看,传递的内容是在线程新建后,如果是线程池则无法读取到父线程的内容
public static InheritableThreadLocal<String> itlHolder = new InheritableThreadLocal<>();
public static ExecutorService executor = Executors.newSingleThreadExecutor();
public static void main(String[] args) throws InterruptedException {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " : " + itlHolder.get());
});
//父线程设置变量
itlHolder.set("hello word");
executor.submit(() -> {
//这里是null
System.out.println(Thread.currentThread().getName() + " : " + itlHolder.get());
});
executor.shutdown();
}
如果要解决线程池下能够正确使用到父线程传递的值,可以关注阿里开源出来的类:TransmittableThreadLocal,本章将不再探讨~
respect!
[如果文章有哪里不对,请指出]