ThreadLocal

  1. Threadlocal 简介

ThreadLocal为每个线程访问的变量提供了一个独立的副本,线程在访问这个变量时,访问的都是自己的副本数据,从而线程安全,即ThreadLocal为变量提供了线程隔离。

  1. 线程是如何隔离的

每一个Thread维护了一个threadLocals变量,这是一个ThreadLocalMap类。

1
2
3
4
5
6
7
java复制代码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副本,实现了变量访问的线程隔离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码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 方法

1
2
3
4
5
6
7
8
9
java复制代码// 我们保存值的时候,是保存在当前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 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码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是将线程需要访问的数据存储在线程对象自身中,从而避免多线程的竞争,实现了线程隔离

  1. 内存泄漏问题

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 如何解决内存泄漏

  1. 使用完ThreadLocal,调用其remove()方法,清除对应的Entry
1
2
3
4
5
6
java复制代码try {
threadLocal.set("张三");
……
} finally {
localName.remove();
}
  1. 当threadLocal被回收,key=null时,当前Thread的ThreadLocalMap每次get、set和remove方法时,都会对key=null的entry进行扫描,同时会把value置为null,从而回收,避免内存泄漏。
  2. 某些层面上,我们可以把ThreadLocal用static声明,从而保证ThreadLocal始终为强引用,不会被回收。
  1. 应用场景

  1. 想象你有一个场景,调用链路非常的长。当你在其中某个环节中查询到了一个数据后,最后的一个节点需要使用一下.这个时候你怎么办?你是在每个接口的入参中都加上这个参数,传递进去,然后只有最后一个节点用吗?
    可以实现,但是不太优雅
  2. 再想想一个场景,你有一个和业务没有一毛钱关系的参数,比如 traceId ,纯粹是为了做日志追踪用。你加一个和业务无关的参数一路透传干啥玩意?

此时就可以选择ThreadLocal.

  1. 链路跟踪中保存线程上下文环境,在一个请求流转中非常方便的获取一些关键信息
  2. PageHelper中,出现不加startPage也会给执行sql添加limit的小错误,可能就是因为ThreadLocal中的数据没有被清除导致的
  3. Spring框架的事务管理中使用ThreadLocal来管理连接,每个线程是单独的连接,当事务失败时不能影响到其他线程的事务过程或结果。Mybatis也是用ThreadLocal管理,SqlSession也是如此
  1. 其他问题

5.1 ThreadLocalMap解决Hash冲突

ThreadLocalMap中并没有使用链表的方式来解决Hash冲突,而是使用的线性探测法,即根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap这里位置是找下一个相邻的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**
* 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 中学来的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%