ThreadLocal大家肯定都不陌生吧,在工作中经常被用到,面试当中基本也是必问的点。
上次面试的时候就栽到ThreadLocal这里了,本以为知道它的应用场景跟使用方法就够了,谁知道面试官懂的太多了。
我回去也是赶紧读了下ThreadLocal的源码,今天和大家分享下。
首先看下ThreadLocal的基本概念,ThreadLocal是一个能够在并发环境下隔离用户线程的同步工具,他通过为每个线程都建立一个本地的副本起到线程之间的共享变量互不影响,主要的应用场景一般为存储上下文,比如用户信息、日志记录、TraceID等。
光看概念是不是挺懵啊,跟着我看下面这三个问题,看完你就不用在死记八股了。
1.ThreadLocalMap是什么?
ThreadLocalMap是ThreadLocal类中的一个静态内部类,并且也是Thread类中的一个属性(重点要记住,作用是跟线程做绑定),这个ThreadLocalMap才是真正存储数据的地方,接着往下看。
2.ThreadLocalMap是如何做到线程隔离的?
看完下面这段代码你就明白了,核心在 getMap(t) 方法上,其实就是获取到当前线程中绑定的ThreadLocalMap,每个线程实例独一份的存在,所以启到线程隔离的作用。(看代码注释)
ThreadLocal中的set()方法👇🏻👇🏻👇🏻
publicvoidset(Tvalue){// 1.先获取当前线程Threadt=Thread.currentThread();// 2.获取当前线程中的ThreadLocalMap属性ThreadLocalMapmap=getMap(t);if(map!=null){// 3.如果ThreadLocalMap不为空,就将当前实例作为key,要存储的数据作为value放到这个Map里。map.set(this,value);}else{// 4.否则就通过当前线程创建一个ThreadLocalMapcreateMap(t,value);}}ThreadLocal中的getMap()方法👇🏻👇🏻👇🏻
ThreadLocalMapgetMap(Threadt){// 拿到当前线程中的ThreadLocalMapreturnt.threadLocals;}ThreadLocal中的createMap()方法👇🏻👇🏻👇🏻
voidcreateMap(Threadt,TfirstValue){// 创建一个ThreadLocalMap赋值给当前线程// 用当前ThreadLocal的实例作为key,用真实数据作为value。t.threadLocals=newThreadLocalMap(this,firstValue);}通过上面三个方法就能确定不同的线程会创建属于自己的ThreadLocalMap,并且通过ThreadLocal的实例this作为Map的key,传入的真实数据作为Map的value,所以我们的数据是因为放到了这个Map里,并且这个Map又绑定到了线程上,所以做到了数据隔离。
接下来再看一段代码👇🏻👇🏻👇🏻
ThreadLocal<String>demo=newThreadLocal<>();// 直接添加valuedemo.set("hello world");不知道你在平时使用的时候有没有想过,为啥在ThreadLocal set数据的时候不用指定key呢?
其实就是上面提到的,源码中特意将ThreadLocal的实例this作为Map的key,那为什么要这么设计呢?
再来看一段代码,在同一线程下创建了两个不同类型不同作用的ThreadLocal,他们之间的值并不会互相干扰,其实就是因为使用this(当前ThreadLocal对象,不同实例有自己的独立空间)作为了ThreadLocalMap的key,如果使用比如字符串作为key就可能出现key冲突,当然这里还涉及到一个内存管理的问题,在下一小节聊。
publicstaticvoidmain(String[]args){ThreadLocal<String>userLocal=newThreadLocal<>();userLocal.set("jay");ThreadLocal<Long>traceLocal=newThreadLocal<>();traceLocal.set(System.currentTimeMillis());}3.ThreadLocal会不会发生内存泄漏?
如果在面试的时候能把上面源码讲明白,想必下一步面试官肯定会问你这个问题,那就是”ThreadLocal会不会发生内存泄漏?“
答案是的,如果使用不当是会发生内存泄漏的。
要搞懂这个问题,就先要了解下ThreadLocalMap的结构👇🏻👇🏻👇🏻
staticclassThreadLocalMap{// Entry继承自WeakReference<ThreadLocal<?>>的弱引用staticclassEntryextendsWeakReference<ThreadLocal<?>>{/** The value associated with this ThreadLocal. */Objectvalue;// Entry的构造方法中把ThreadLocal作为key给到了父类的构造方法Entry(ThreadLocal<?>k,Objectv){super(k);value=v;}}privatestaticfinalintINITIAL_CAPACITY=16;privateEntry[]table;// Entry类型的数组privateintsize=0;privateintthreshold;// 省略了一些其他方法ThreadLocalMap中维护了一个Entry类型的数组,在set数据的时候是这样的顺序:调用ThreadLocal的set()方法 -> 调用ThreadLocalMap的set()方法 -> 调用Entry的构造方法初始化数据。
这个Entry类继承了一个WeakReference<ThreadLocal<?>>的弱引用,然后Entry的构造方法中把ThreadLocal作为key给到了父类的构造方法,所以Entry数组对 ThreadLocal实例(Key)是弱引用。
经过上面的分析,我们得到Entry数组的key(ThreadLocal实例)是一个弱引用,意味着如果这个 ThreadLocal实例在程序的其他地方没有被任何强引用指向了(比如你将其设置为 null),那么下次垃圾回收(GC)时,这个 ThreadLocal 实例本身会被回收。此时,Entry中的 Key 会变为 null。
那什么情况下就是没有被强引用指向了呢?
- 声明为局部变量的时候,在方法内部创建并使用 ThreadLocal(未将其返回或赋值给类成员变量/静态变量),当方法执行完时ThreadLocal的栈帧会被销毁。
- 匿名对象,直接使用 new ThreadLocal().set(value) 而未将 new 出来的对象赋给任何变量。
- 非静态成员变量,将 ThreadLocal 声明为某个类的普通成员变量(非static),当持有它的那个类实例被垃圾回收时,该 ThreadLocal 实例的强引用也随之消失。
我们现在已经知道Entry数组的key是一个弱引用了,但Entry的value并不是一个弱引用,反而它是一个强引用,意味着即使 Key 已经被回收了,那个存储在 Entry里的、可能非常大的 Value 对象(比如一个大的缓存),依然被这个 Entry强引用着,只要存放它的线程还存在,这个 Value 就无法被 GC 回收。
下面做个总结,哪些情况容易引起内存泄漏?
- Entry数组的key被强引用,也就是将ThreadLocal声明为了静态的成员变量。
- Entry数组的value本身就是一个强引用。
- 使用线程池中的线程,线程池中的核心线程会重复利用,不会像普通线程一样被销毁。
该如何有效避免内存泄漏?
其实解决方案就一句话👇🏻👇🏻👇🏻,但是请记住:我们要的不是结果。
最近看到一个很扎心的现象:企业越来越关注开发效率,而 AI 正在成为新的生产力工具。同样的需求,会使用 AI 的工程师往往能够更快完成设计、编码和测试工作。与其担心被 AI 替代,不如尽早学会驾驭 AI。最近我不仅在学习 Java 底层,还在学习一些人工智能的知识,发现了一个不错的 AI 学习网站,内容通俗易懂,比较适合程序员快速上手,感兴趣的话也可以看看:人工智能学习网
在 ThreadLocal使用完毕后,务必调用 remove() 方法来清除当前线程中存储的值,最好是使用 try-finally块确保清理一定会执行。