06. ThreadLocal

3y 2021-08-19 13:59:24
Categories: Tags:

面试官今天要不来聊聊ThreadLocal吧?

候选者:我个人对ThreadLocal理解就是

候选者:它提供了线程的局部变量,每个线程都可以通过set/get来对这个局部变量进行操作

候选者:不会和其他线程的局部变量进行冲突,实现了线程的数据隔离

面试官你在工作中有用到过ThreadLocal吗?

候选者:这块是真不多,不过还是有一处的。就是我们项目有个的DateUtils工具类

候选者:这个工具类主要是对时间进行格式化

候选者:格式化/转化的实现是用的SimpleDateFormat

候选者:但众所周知SimpleDateFormat不是线程安全的,所以我们就用ThreadLocal来让每个线程装载着自己的SimpleDateFormat对象

候选者:以达到在格式化时间时,线程安全的目的

候选者:在方法上创建SimpleDateFormat对象也没问题,但每调用一次就创建一次有点不优雅

候选者:在工作中ThreadLocal的应用场景确实不多,但要不我给你讲讲Spring是怎么用的?

面试官:好吧,你讲讲呗

候选者:Spring提供了事务相关的操作,而我们知道事务是得保证一组操作同时成功或失败的

候选者:这意味着我们一次事务的所有操作需要在同一个数据库连接上

候选者:但是在我们日常写代码的时候是不需要关注这点的

候选者:Spring就是用的ThreadLocal来实现,ThreadLocal存储的类型是一个Map

候选者:Map中的key 是DataSource,value 是Connection(为了应对多数据源的情况,所以是一个Map)

候选者:用了ThreadLocal保证了同一个线程获取一个Connection对象,从而保证一次事务的所有操作需要在同一个数据库连接上

面试官:了解

面试官你知道ThreadLocal内存泄露这个知识点吗?

候选者:怎么都喜欢问这个…

候选者:了解的,要不我先来讲讲ThreadLocal的原理?

面试官:请开始你的表演吧

候选者:ThreadLocal是一个壳子,真正的存储结构是ThreadLocal里有ThreadLocalMap这么个内部类

候选者:而有趣的是,ThreadLocalMap的引用是在Thread上定义的

候选者:ThreadLocal本身并不存储值,它只是作为key来让线程从ThreadLocalMap获取value

候选者:所以,得出的结论就是ThreadLocalMap该结构本身就在Thread下定义,而ThreadLocal只是作为key,存储set到ThreadLocalMap的变量当然是线程私有的咯

面试官:那我想问下,我可以在ThreadLocal下定义Map,key是Thread,value是set进去的值吗?

面试官:就是说,为啥我要把ThreadLocal做为key,而不是Thread做为key?这样不是更清晰吗?

候选者:嗯,我明白你的意思。

候选者:理论上是可以,但没那么优雅。

候选者:你提出的做法实际上就是所有的线程都访问ThreadLocal的Map,而key是当前线程

候选者:但这有点小问题,一个线程是可以拥有多个私有变量的嘛,那key如果是当前线程的话,意味着还点做点「手脚」来唯一标识set进去的value

候选者:假设上一步解决了,还有个问题就是;并发量足够大时,意味着所有的线程都去操作同一个Map,Map体积有可能会膨胀,导致访问性能的下降

候选者:这个Map维护着所有的线程的私有变量,意味着你不知道什么时候可以「销毁」

候选者:现在JDK实现的结构就不一样了。

候选者:线程需要多个私有变量,那有多个ThreadLocal对象足以,对应的Map体积不会太大

候选者:只要线程销毁了,ThreadLocalMap也会被销毁

面试官:嗯,了解。

面试官回到ThreadLocal内存泄露上吧,谈谈你对这个的理解呗

候选者:ThreadLocal内存泄露其实发生的概率非常非常低,我也不知道为什么这么喜欢问。

候选者:回到原理上,我们知道Thread在创建的时候,会有栈引用指向Thread对象,Thread对象内部维护了ThreadLocalMap引用

候选者:而ThreadLocalMap的Key是ThreadLocal,value是传入的Object

候选者:ThreadLocal对象会被对应的栈引用关联,ThreadLocalMap的key也指向着ThreadLocal

候选者:ThreadLocalRef && ThreadLocalMap Entry key ->ThreadLocal

候选者:ThreadRef->Thread->ThreadLoalMap-> Entry value-> Object

候选者:网上大多分析的是ThreadLocalMap的key是弱引用指向ThreadLocal

面试官:嗯…要不顺便讲讲Java的4种引用吧

候选者:强引用是最常见的,只要把一个对象赋给一个引用变量,这个引用变量就是一个强引用

候选者:强引用:只要对象没有被置null,在GC时就不会被回收

候选者:软引用相对弱化了一些,需要继承 SoftReference实现

候选者:软引用:如果内存充足,只有软引用指向的对象不会被回收。如果内存不足了,只有软引用指向的对象就会被回收

候选者:弱引用又更弱了一些,需要继承WeakReference实现

候选者:弱引用:只要发生GC,只有弱引用指向的对象就会被回收

候选者:最后就是虚引用,需要继承PhantomReference实现

候选者:虚引用的主要作用是:跟踪对象垃圾回收的状态,当回收时通过引用队列做些「通知类」的工作

候选者:了解了这几种引用之后,再回过头来看ThreadLocal

面试官:嗯..

候选者:ThreadLocal内存泄露指的是:ThreadLocal被回收了,ThreadLocalMap Entry的key没有了指向

候选者:但Entry仍然有ThreadRef->Thread->ThreadLoalMap-> Entry value-> Object 这条引用一直存在导致内存泄露

面试官:嗯..

候选者:为什么我说导致内存泄露的概率非常低呢,我觉得是这样的

候选者:首先ThreadLocal被两种引用指向

候选者:1):ThreadLocalRef->ThreadLocal(强引用)

候选者:2):ThreadLocalMap Entry key ->ThreadLocal(弱引用)

候选者:只要ThreadLocal没被回收(使用时强引用不置null),那ThreadLocalMap Entry key的指向就不会在GC时断开被回收,也没有内存泄露一说法

候选者:通过ThreadLocal了解实现后,又知道ThreadLocalMap是依附在Thread上的,只要Thread销毁,那ThreadLocalMap也会销毁

候选者:那非线程池环境下,也不会有长期性的内存泄露问题

候选者:而ThreadLocal实现下还做了些”保护“措施,如果在操作ThreadLocal时,发现key为null,会将其清除掉

候选者:所以,如果在线程池(线程复用)环境下,如果还会调用ThreadLocal的set/get/remove方法

候选者:发现key为null会进行清除,不会有长期性的内存泄露问题

候选者:那存在长期性内存泄露需要满足条件:ThreadLocal被回收&&线程被复用&&线程复用后不再调用ThreadLocal的set/get/remove方法

候选者:使用ThreadLocal的最佳实践就是:用完了,手动remove掉。就像使用Lock加锁后,要记得解锁

面试官:那我想问下,为什么要将ThreadLocalMap的key设置为弱引用呢?强引用不香吗?

候选者:外界是通过ThreadLocal来对ThreadLocalMap进行操作的,假设外界使用ThreadLocal的对象被置null了,那ThreadLocalMap的强引用指向ThreadLocal也毫无意义啊。

候选者:弱引用反而可以预防大多数内存泄漏的情况

候选者:毕竟被回收后,下一次调用set/get/remove时ThreadLocal内部会清除掉

面试官我看网上有很多人说建议把ThreadLocal修饰为static,为什么?

候选者:ThreadLocal能实现了线程的数据隔离,不在于它自己本身,而在于Thread的ThreadLocalMap

候选者:所以,ThreadLocal可以只初始化一次,只分配一块存储空间就足以了,没必要作为成员变量多次被初始化。

面试官:最后想问个问题:什么叫做内存泄露?

候选者:…..

候选者:意思就是:你申请完内存后,你用完了但没有释放掉,你自己没法用,系统又没法回收。

面试官:清楚了

本文总结

第一时间获取BATJTMD一线互联网大厂最新的面试资料以及内推机会关注公众号「对线面试官