1. Threadlocal 简介
ThreadLocal为每个线程访问的变量提供了一个独立的副本,线程在访问这个变量时,访问的都是自己的副本数据,从而线程安全,即ThreadLocal为变量提供了线程隔离。
2. 线程是如何隔离的
每一个Thread维护了一个threadLocals变量,这是一个ThreadLocalMap类。
public class Thread implements Runnable {
... ...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
... ...
}
复制代码
ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解,当前threadLocal对象作为key,变量值作为value),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。
ThreadLocal类通过操作每个线程独有的ThreadLocalMap副本,实现了变量访问的线程隔离。
public class ThreadLocal<T> {
... ...
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
复制代码
Set 方法
// 我们保存值的时候,是保存在当前Thread自己的ThreadLocalMap中,属于自己的独立副本,别人无法访问
public void set(T value) {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap(每个线程拥有自己独立的副本)
if (map != null)
map.set(this, value); // this,即threadLocal作为Key
else
createMap(t, value); // 没有则创建
}
复制代码
Get 方法
public T get() {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap(每个线程拥有自己独立的副本)
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
复制代码
总结来说就是,ThreadLocal是将线程需要访问的数据存储在线程对象自身中,从而避免多线程的竞争,实现了线程隔离。
3. 内存泄漏问题
3.1 弱引用
我们可以注意到在ThreadLocalMap的Entry中,ThreadLocal作为key使用了弱引用。为什么使用弱引用呢?使用强引用会有什么问题?我们先回顾下Java的引用
- 强引用:一个对象具有强引用,那么一定不会被gc回收;即使内存不足时,也不会回收
- 软引用:可有可无,当内存不够时,不会回收;当内存不足时,就会回收
- 弱引用:比软引用具有更短的生命周期。当垃圾回收器扫描到这部分内存,发现弱引用,无论内存是否足够,都会把它回收掉.
- 虚引用:并不能决定对象的生命周期。任何时候都可能被回收。
3.1.1 为什么不使用强引用?
当我们使用完ThreadLocal准备释放它时,比如置为null,而此时ThreadLocalMap的ThreadLocal因为具有强引用,会导致不能被回收,从而可能会造成内存泄漏,且不符合用户预期的结果。
3.1.2 为什么使用弱引用?
为了解决使用强引用内存泄漏的问题。我们使用完把它置为null时,此时ThreadLocalMap中的ThreadLocal因为只具有弱引用,所以很容易被gc回收掉,从而释放掉,符合用户预期结果。
3.2 内存泄漏
使用弱引用就不会有问题了吗?仍然会有内存泄漏的问题。
当ThreadLocal由于弱引用被gc回收时,此时键值对中的value由于是强引用,所以此时并没有被回收,如果当前线程一直在持续工作(比如线程池中的线程持续复用),那么value会始终存在一条强引用链,而导致不能被回收,从而造成内存泄漏
CurrentThread--->ThreadLocalMap--->Entry--->value
3.3 如何解决内存泄漏
- 使用完ThreadLocal,调用其remove()方法,清除对应的Entry
try {
threadLocal.set("张三");
……
} finally {
localName.remove();
}
复制代码
- 当threadLocal被回收,key=null时,当前Thread的ThreadLocalMap每次get、set和remove方法时,都会对key=null的entry进行扫描,同时会把value置为null,从而回收,避免内存泄漏。
- 某些层面上,我们可以把ThreadLocal用static声明,从而保证ThreadLocal始终为强引用,不会被回收。
4. 应用场景
- 想象你有一个场景,调用链路非常的长。当你在其中某个环节中查询到了一个数据后,最后的一个节点需要使用一下.这个时候你怎么办?你是在每个接口的入参中都加上这个参数,传递进去,然后只有最后一个节点用吗?
可以实现,但是不太优雅
2. 再想想一个场景,你有一个和业务没有一毛钱关系的参数,比如 traceId ,纯粹是为了做日志追踪用。你加一个和业务无关的参数一路透传干啥玩意?
此时就可以选择ThreadLocal.
- 链路跟踪中保存线程上下文环境,在一个请求流转中非常方便的获取一些关键信息
- PageHelper中,出现不加startPage也会给执行sql添加limit的小错误,可能就是因为ThreadLocal中的数据没有被清除导致的
- Spring框架的事务管理中使用ThreadLocal来管理连接,每个线程是单独的连接,当事务失败时不能影响到其他线程的事务过程或结果。Mybatis也是用ThreadLocal管理,SqlSession也是如此
5. 其他问题
5.1 ThreadLocalMap解决Hash冲突
ThreadLocalMap中并没有使用链表的方式来解决Hash冲突,而是使用的线性探测法,即根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap这里位置是找下一个相邻的位置。
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
复制代码
5.2 扩展
Dubbo中的InternalThreadLocal 是 ThreadLocal 的一个变种,当配合 InternalThread 使用时,具有比普通 Thread 更高的访问性能。
InternalThread 的内部使用的是数组,通过下标定位,非常的快。如果遇得扩容,直接数组扩大一倍,完事。
而 ThreadLocal 的内部使用的是 hashCode 去获取值,多了一步计算的过程,而且用 hashCode 必然会遇到 hash 冲突的场景,ThreadLocal 还得去解决 hash 冲突,如果遇到扩容,扩容之后还得 rehash ,就会慢一些
数据结构都不一样了,这其实就是这两个类的本质区别,也是 InternalThread 的性能在 Dubbo 的这个场景中比 ThreadLocal 好的根本原因。
而 InternalThread 这个设计思想是从 Netty 的 FastThreadLocal 中学来的。
近期评论