1.ThreadLocal 是什么?
ThreadLocal 是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,适用于各个线程不共享变量值的操作。
2. ThreadLocal 工作原理是什么?
TreadLocal 原理:每个线程的内部都维护了一个 ThreadLocalMap,它是一个 Map(key,value)数据格式,key 是一个弱引用,也就是 ThreadLocal 本身,而 value 存的是线程变量的值。也就是说 ThreadLocal 本身并不存储线程的变量值,它只是一个工具,用来维护线程内部的 Map,帮助存和取变量。数据结构,如下图所示:
3.ThreadLocal 如何解决 Hash 冲突?
与 HashMap 不同,ThreadLocalMap 结构非常简单,没有 next 引用,也就是说 ThreadLocalMap 中解决 Hash 冲突的方式并非链表的方式,而是采用线性探测的方式。所谓线性探测,就是根据初始 key 的 hashcode 值确定元素在 table 数组中的位置,如果发现这个位置上已经被其他的 key 值占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。源代码实现如下:
4.ThreadLocal 的内存泄露是怎么回事?
最主要的原因在于它的内部类 ThreadLocalMap 中的 Entry 的设计。Entry 继承了 WeakReference<ThreadLocal<?>>,即 Entry的 key 是弱引用。因此如果 ThreadLocal 没有外部强引用来引用它,那么 ThreadLocal 会在下次 JVM 垃圾收集时被回收。这个时候 Entry 中的 key 已经被回收,但是 value 又是一强引用不会被垃圾收集器回收,这样 ThreadLocal 的线程如果一直持续运行,value 就一直得不到回收,这样就会发生内存泄露。
5. ThreadLocalMap 的 key 是弱引用的原因?
我们知道 ThreadLocalMap 中的 key 是弱引用,而 value 是强引用才会导致内存泄露的问题,至于为什么要这样设计,这样分为两种情况来讨论:
- key 使用强引用:这样会导致一个问题,引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,则会导致内存泄漏。
- key 使用弱引用:这样的话,引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候会被清除。
比较以上两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障,弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候被清除,算是最优的解决方案。最佳实践是,应该在我们不使用的时候,主动调用remove方法进行清理。示例代码如下:
抽象为:
try {
// 其它业务逻辑
} finally {
threadLocal对象.remove();
}
6.ThreadLocal 的应用场景有哪些?
- 保存线程上下文信息,在任意需要的地方可以获取!!!
- 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失!!!
保存线程上下文信息,在任意需要的地方可以获取!!!由于ThreadLocal的特性,同一线程在某地方进行设置,在随后的任意地方都可以获取到。从而可以用来保存线程上下文信息。常用的比如每个请求怎么把一串后续关联起来,就可以用ThreadLocal进行set,在后续的任意需要记录日志的方法里面进行get获取到请求id,从而把整个请求串起来。还有比如Spring的事务管理,用ThreadLocal存储Connection,从而各个DAO可以获取同一Connection,可以进行事务回滚,提交等操作。
线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失!!!ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。但是ThreadLocal也有局限性,我们来看看阿里规范:
这里把ThreadLocal定义为static还有一个好处就是,由于ThreadLocal有强引用在,那么在ThreadLocalMap里对应的Entry的键会永远存在,那么执行remove的时候就可以正确进行定位到并且删除。
每个线程往ThreadLocal中读写数据是线程隔离,互相之间不会影响的,所以ThreadLocal无法解决共享对象的更新问题!由于不需要共享信息,.自然就不存在竞争问题了,从而保证了某些情况下线程的安全,以及避免了某些情况需要考虑线程安全必须同步带来的性能损失。这类场景阿里规范里面也提到了:
7. ThreadLocalMap 源码解析
ThreadLocalMap是一个Map,key是ThreadLocal,value是Object。映射到源码就是如下所示:ThreadLocalMap是ThreadLocal的一个静态内部类:
public class Thread implements Runnable{
ThreadLocal.ThreadLocalMap threadLocals = null;
}
往ThreadLocalMap里面放值:
public void set(T value){
Thread t = Thread.currentThread();
ThreadLocalMap map = t.threadLocals;
if(map != null){
map.set(this, value);
}else{
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
往ThreadLocalMap里面取值:
//ThreadLocal类里的方法
public T get(){
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if(map != null){
ThreadLocalMap.Entity e = map.getEntry(this);
if(e != null){
@SuppressWarning("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
8. Thread 和 ThreadLocal 之间的联系
Thread和ThreadLocal是绑定的, ThreadLocal依赖于Thread去执行, Thread将需要隔离的数据存放到ThreadLocal(准确的讲是ThreadLocalMap)中, 来实现多线程处理。
9. ThreadLocal 在Spring 中的应用
ThreadLocal 天生为解决相同变量的访问冲突问题, 所以这个对于 Spring 的默认单例 bean 的多线程访问是一个完美的解决方案。Spring 也确实是用了 ThreadLocal 来处理多线程下相同变量并发的线程安全问题。
在Spring 中保证数据库事务在同一个连接下执行也使用了 ThreadLocal 。要想实现jdbc事务, 就必须是在同一个连接对象中操作, 多个连接下事务就会不可控, 需要借助分布式事务完成。在 Spring 中 DataSourceTransactionManager 是 Spring 的数据源事务管理器, 它会在你调用 getConnection() 的时候从数据库连接池中获取一个 connection, 然后将其与 ThreadLocal 绑定, 事务完成后解除绑定。这样就保证了事务在同一连接下完成。概要源码如下:
事务开始阶段:
事务结束阶段: