最近的面试题涉及到多线程的地方,会问到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 来源:知乎