ThreadLocal

2,043 阅读6分钟

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

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

4. 应用场景

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

此时就可以选择ThreadLocal.

  1. 链路跟踪中保存线程上下文环境,在一个请求流转中非常方便的获取一些关键信息
  2. PageHelper中,出现不加startPage也会给执行sql添加limit的小错误,可能就是因为ThreadLocal中的数据没有被清除导致的
  3. 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 中学来的。