ThreadLocal源码解析及避坑指南

ThreadLocal 是线程私有领地,其他线程无法访问。
本文首先给出一个简单的使用示例,接下来主要解析ThreadLocal的原理,继而作图以生动地表示ThreadLocal的底层存储关系,探索可能出现的内存泄漏,最后给出一个ThreadLocal的正确使用姿势。

一个初级小栗子

你知道吗?据说这段代码可能会造成内存泄漏,,,什么,不知道?
无妨,写这篇文章之前偶也不知道,一起来学习下吧。

public static void main(String[] args) {
	//tl 是强引用,指向ThreadLocal对象
    final ThreadLocal<String> tl = new ThreadLocal<>();
    tl.set("ThreadLocal Test");
    new Thread(() -> {
        System.out.println("t1 print tl: " + tl.get());
    }, "t1").start();
    System.out.println("main thread print tl: " + tl.get());
}

解析ThreadLocal

明确一个问题

我们知道ThreadLocal用于存储线程私有变量,那么变量存在哪里呢?

答:

见Thread类中的一行代码。私有变量就存储在线程自己的threadLocals中,它是一个ThreadLocalMap。其中ThreadLocal对象作为map的key,存储的变量作为map的值。

ThreadLocal.ThreadLocalMap threadLocals = null;

主要源码分析

public void set(T value) {
    //拿到当前线程 t
    Thread t = Thread.currentThread();
    /**
     * 取得当前线程 t 的threadLocals ,
     * 上文提到过,Thread类中有一个ThreadLocal.ThreadLocalMap 
     * 类型的threadLocals
     */
    ThreadLocalMap map = getMap(t);
    if (map != null)
    	//把变量保存到map中,此处的this代表当前ThreadLocal对象---tl
    	//value代表存储的变量---"ThreadLocal Test"
        map.set(this, value);
    else
        createMap(t, value);
}
public T get() {
	//获取当前线程 t
    Thread t = Thread.currentThread();
    //拿到 t 的threadLocals
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    	//以ThreadLocal对象为key,查询对应的私有变量
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

以上,set、get方法源码解析完毕,只有寥寥数行代码,相当简单。若还是似懂非懂,无妨,看我作图:
在这里插入图片描述

该图描述了ThreadLocal变量的存储关系。分析一下这个图:

  • Thread中持有对ThreadLocal对象的引用 tl,这是一个强引用(R1);还持有对ThreadLocalMap 的引用 threadLocals,这也是一个强引用(R3)。
  • ThreadLocalMap中的key引用了ThreadLocal对象,这是一个弱引用(R2)。
  • ThreadLocalMap中的每一个Entry都继承自WeakRefence。

内存泄漏分析

  1. 为什么R2是弱引用?
    答:弱引用特性:只有弱引用指向的对象,每次gc都会被回收。这样子,当R1断开后,因为唯一指向ThreadLocal对象的引用R2是弱引用,在下次gc时ThreadLocal对象就会被回收,从而不会造成内存泄漏。

    tips:因此每次使用完ThreadLocal,要记得显示指定 tl = null;

  2. 还存在内存泄漏吗?
    答:存在。
    若ThreadLocal对象被回收,那么key就会变为null,那么我们再也无法通过原来的key值获得其对应的value。
    因为R3是强引用,所以ThreadLocalMap会一直存在,进而Value被ThreadLocalMap通过R4(强引用)所引用,也会一直存在。
    Value一直存在于内存,而再也不会被访问到,这也是内存泄漏。

    tips:每次用完ThreadLocal,显示地删除这个变量。tl.remove();

完善的小栗子

经过以上学习,下面给出一个笔者认为完善的示例,防止内存泄漏。

public static void main(String[] args) {
     ThreadLocal<String> tl = new ThreadLocal<>();
     tl.set("ThreadLocal Test");
     //省略中间的应用逻辑
     ...
     //防止value内存泄漏
     tl.remove();
     //防止key内存泄漏
     tl = null;
}

应用场景

  1. 框架中的应用。Spring采用ThreadLocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接。
  2. 实际工作中的应用。我们知道SimpleDataFormat不是线程安全的。所以如果声明为全局变量会有并发安全隐患。一般会在使用这个类的方法里创建SimpleDataFormat对象,这样不会有并发隐患。但是方法内部的对象在方法执行完其生命周期就结束了,如果一个请求在调用链路的n个方法中都调用了SimpleDataFormat,是不是要创建n个对象?此时我们可以将其声明为ThreadLocal,一个线程在其生命周期内使用同一个SimpleDataFormat,不同对象之间使用各自的SimpleDataFormat对象,形成线程间的数据隔离,不会有并发安全隐患。然而这种场景,适用于一个线程有多个方法调用都用到了SimpleDataFormat,可以减少创建对象的次数。如果生命周期内只有一个方法使用了这个对象,在方法里创建就好了。
  3. 用于传递参数。项目中,一个线程横跨若干个方法调用,需要传递的对象,也就是上下文,它是一种状态,经常就是用户身份、任务信息等,就会存在过渡传参的问题。在线程最开始设置ThreadLocal对象,在线程生命周期的多个方法执行过程中,通过tl.get()就可以取出参数。这种方法可以避免通过方法入参传递变量。

补充

Thread中还存在一个属性inheritableThreadLocals,可以继承父线程中的inheritableThreadLocals 的值。

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

创建线程对象的时候,有如下一行代码。若inheritThreadLocals 是true,且父线程的inheritableThreadLocals 不是null,则当前线程的inheritableThreadLocals 使用父线程中inheritableThreadLocals 的值。

	if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

版权声明:本文为tyx001174原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。