ThreadLocal的原理和分析

ThreadLocal简介

ThreadLocal就是一个类,它有get、set方法,可以起到一个保存、获取某个值的作用。但是这个类的get、set方法有点特殊,各个线程调用时是互不干扰的(变量副本),就好像线程在操作ThreadLocal对象时是在操作线程自己的私有属性一样。具体原因在于他的方法实现:

public T get() {
    Thread t = Thread.currentThread();  //先确定调用我的线程
    ThreadLocalMap map = getMap(t);  //根据调用我的线程,找到这个线程的ThreadLocalMap对象
    if (map != null) {
    	ThreadLocalMap.Entry e = map.getEntry(this);  //以ThreadLocal对象为key,找到对应元素
    	if (e != null) {
    		@SuppressWarnings("unchecked")
    		T result = (T)e.value;   //讲元素的value返回
    		return result;
    	}
    }
    return setInitialValue();  //如果调用我的线程没有ThreadLocalMap对象,则返回初始值
}
public void set(T value) {
    Thread t = Thread.currentThread();  //先确定调用我的是哪个线程
    ThreadLocalMap map = getMap(t);  //获取调用我的线程的ThreadLocalMap 
    if (map != null)
    	map.set(this, value);  //如果那个线程有map,就将此ThreadLocal对象为key的value设置好
    else
    	createMap(t, value);   //如果那个线程还没有map,先创建一个再设置
}

ThreadLocalMap是ThreadLocal的内部类,为了不造成混乱,可以把它看作一个普通的类。ThreadLocalMap其实类似HashMap,也是通过key获取某个值(key就是ThreadLocal对象),也是数组存储键值对,拉链法解决冲突等。一个Thread类持有一个ThreadLocalMap实例

通过上面的源码也可以看出:线程互不干扰的操作ThreadLocal的原因就是,它的set、get方法是要先获取当前线程,然后修改、操作这个线程对象的成员属性。也就是说,调用ThreadLocal对象的set、get方法实际上是在操作当前线程的成员属性,只不过这些属性是通过ThreadLocal对象为key找到的而已。为了直观明了,看下图:

简单概括过程:有ThreadLocal对象 tl,线程 t 调用 tl.get(), 则去线程 t 的ThreadLocalMap属性对象里找到一个entry,若entry.key == tl返回true,则此entry是目标entry,此entry.value就是我们的目标。

ThreadLocal对象只是一个获取当前线程某个私有属性的渠道而已,提供了set、get的入口,同时作为key去获取和设置目标值。真正的有效目标是属于线程对象私自持有的,自然通过ThreadLocal对象获取的值也就不会受其他线程影响啦

ThreadLocal使用方式

ThreadLocal 的使用非常简单,最核心的操作就是四个:创建、创建并赋初始值、赋值、取值。

1、创建

ThreadLocal<String> mLocal = new ThreadLocal<>();

2、创建并赋初值。下面代码表示创建了一个 String 类型的 ThreadLocal 并且重写了 initialValue 方法,并返回初始字符串,之后调用 get() 方法获取的值便是 initialValue 方法返回的值。

private static ThreadLocal<String> mLocal = new ThreadLocal<String>(){
    @Override
    protected String initialValue(){
        return "init value";
    }
};
System.out.println(mLocal.get());

3、设置值

mLocal.set("hello");

4、取值

mLocal.get()

ThreadLocal实现原理

首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象

因为一个线程内可以存在多个 ThreadLocal 对象,所以ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类(key为当前的ThreadLocal对象)。而我们使用的 get()、set() 方法其实都是调用了这个 ThreadLocalMap 类对应的 get()、set() 方法。例如下面的 set 方法:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

调用 ThreadLocal 的 set 方法时,首先获取到了当前线程,然后获取当前线程维护的 ThreadLocalMap 对象,最后在ThreadLocalMap 实例中添加上。如果 ThreadLocalMap 实例不存在则初始化并赋初始值。

这里看到 set 方法的第一个参数是 this ,this即指的是当前的 ThreadLocal 对象,看上面的代码就是指的 mLocal 这个对象。而在 ThreadLocalMap 的 set 方法中会根据当前 ThreadLocal 对象实例,做一些操作和判断,最终实现赋值操作。

所以说,最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是一个中间工具,传递了变量值。

关于Entry的弱类型引用

如果阅读ThreadLocalMap的Entry源码会发现,Entry的key是弱引用:

为什么要这么做呢?看下面的这种场景:

public void func1() {
    ThreadLocal tl = new ThreadLocal<Integer>(); //line1
    tl.set(100);   //line2
    tl.get();       //line3
}

line1新建了一个ThreadLocal对象,t1 是强引用指向这个对象;line2调用set()后,新建一个Entry,通过源码可知entry对象里的 k是弱引用指向这个对象。如图:

当func1方法执行完毕后,栈帧销毁,强引用 tl 也就没有了,但此时线程的ThreadLocalMap里某个entry的 k 引用还指向这个对象。若这个k 引用是强引用,就会导致k指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏,但是弱引用就不会有这个问题(弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收,而且在entry的k引用为null后,再调用get,set或remove方法时,就会尝试删除key为null的entry,可以释放value对象所占用的内存。

概括说就是:在方法中新建一个ThreadLocal对象,就有一个强引用指向它,在调用set()后,线程的ThreadLocalMap对象里的Entry对象又有一个引用 k 指向它。如果后面这个引用 k 是强引用就会使方法执行完,栈帧中的强引用销毁了,对象还不能回收,造成严重的内存泄露。

注意:虽然弱引用,保证了k指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现k为null时才会去回收整个entry、value,因此弱引用不能保证内存完全不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。

ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长(即线程结束了ThreadLocalMap也没了),如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal的使用场景

  • 比如线程中处理一个非常复杂的业务,可能方法有很多,那么,使用 ThreadLocal 可以代替一些参数的显式传递;
  • 比如用来存储用户 Session。Session 的特性很适合 ThreadLocal ,因为 Session 之前当前会话周期内有效,会话结束便销毁。
  • 在一些多线程的情况下,如果用线程同步的方式,当并发比较高的时候会影响性能,可以改为 ThreadLocal 的方式,例如高性能序列化框架 Kyro 就要用 ThreadLocal 来保证高性能和线程安全;
  • 数据库连接等

ThreadLocal总结

(1)每个Thread对象内部都有一个ThreadLoacalMap的成员变量,这个变量类似一个Map类型,其中key为我们定义的ThreadLocal变量的this引用,value则为我们使用set方法设置的值;

(2)内存泄漏

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:

value永远无法回收,造成内存泄漏。

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

如何避免内存泄漏(阿里代码规约)

  • 调用remove()方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal之后,应该调用remove()方法
  • 如果使用拦截器获取用户信息,那么同样应该使用拦截器在线程请求退出之前将之前保存过得信息清除掉

 

posted @ 2022-01-12 21:06  残城碎梦  阅读(67)  评论(0编辑  收藏  举报