【每日面试】Java中的ThreadLocal
最近的面试题涉及到多线程的地方,会问到ThreadLocal,研究一下。
思路是这样的:
1)先按照五步法则去分析
2)查阅api手册+源码+官方解释
3)搜索相关文章,借鉴经验
4)实际使用,感受优、缺点
5)在项目中的应用场景
1.
五步法则:是什么?有什么用?用在哪里?怎么用?为什么这样用?
1)ThreadLocal是什么?
查看JDK的api手册,发现ThreadLocal是lang包下的一个泛型类,从1.2版本开始就存在了,有一个子类InheritableThreadLocal,摘抄一段api中的解释:
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
JDK6API文档
它拥有一个构造方法+4个普通方法+1个静态方法,看起来特别简单
//构造方法: ThreadLocal() 创建一个线程本地变量。 //普通方法: T get() 返回此线程局部变量的当前线程副本中的值。 protected T initialValue() 返回此线程局部变量的当前线程的“初始值”。 void remove() 移除此线程局部变量当前线程的值。 void set(T value) 将此线程局部变量的当前线程副本中的值设置为指定值。 //静态方法: public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)
2)ThreadLocal有什么用?
定义线程级别的全局变量,解决线程中相同变量的访问冲突
3)ThreadLocal使用场景?
spring框架源码中TransactionSynchronizationManager类中实现事务隔离级别。
之前我们上线后发现部分用户的日期居然不对了,排查下来是SimpleDataFormat的锅,当时我们使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方***先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。
其实要解决这个问题很简单,让每个线程都new 一个自己的 SimpleDataFormat就好了,但是1000个线程难道new1000个SimpleDataFormat?
所以当时我们使用了线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。
在项目中存在一个线程经常遇到横跨若干方法调用,需要传递的对象,也就是上下文(Context),它是一种状态,经常就是是用户身份、任务信息等,就会存在过渡传参的问题。
使用到类似责任链模式,给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,对象参数就传不进去了,所以我使用到了ThreadLocal去做了一下改造,这样只需要在调用前在ThreadLocal中设置参数,其他地方get一下就好了。
4)ThreadLocal如何使用?
直接new即可,因为方法比较简单,基础的就是放、取、删除
5)ThreadLocal使用原理?
ThreadLocal 变量只在单个线程内可见
ThreadLocal 对象存储在堆上
因为ThreadLocal的变量是保存在每一个线程的map中:
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap底层是数组,初始化长度16
ThreadLocalMap的key是弱引用,在GC时会直接被回收
2.
1)优点
使用简单,多线程操作时全局存储变量,跨线程变量传递
2)缺点
用不好容易内存泄漏,使用场景不好掌控
3.
存在的一些问题:
1)内存泄漏
ThreadLocalMap中的key是弱引用,但value是强引用,线程不被回收,value也不会被回收,但大部分线程都是在线程池中被重复使用的,所以就会出现内存泄漏问题。解决方法是在set(),get(),remove()的时候,进行清理,但这不能肯定保证不再泄漏,比如总是访问固定几个一直存在的ThreadLocal,清理动作不会执行,就还是会有泄漏的可能。好的习惯是,当不再使用这个ThreadLocal时,主动调用remove()清除。
2)Hash冲突
使用的是简单的线性探测法,如果发生了元素冲突,那么就使用下一个槽位存放
3)父子线程间数据传递
使用InheritableThreadLocal可以实现,但要注意:
变量的传递是发生在线程创建的时候,如果不是新建线程,而是用了线程池里的线程,就不灵了
变量的赋值就是从主线程的 map 复制到子线程,它们的 value 是同一个对象,如果这个对象本身不是线程安全的,那么就会有线程安全问题
参考文章:
撸完腾讯 T4 大佬整理的 ThreadLocal 笔记,解决内存泄漏只是小儿科
https://xie.infoq.cn/article/79b0c1679a6f97edda34ab3c6
Java中ThreadLocal的实际用途是啥?
作者:敖丙 链接:https://www.zhihu.com/question/341005993/answer/1367225682 来源:知乎
面试分析,在机会来临前做好准备。