ThreadLocal是什么?有哪些用途?

ThreadLocal是什么?

该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

以上来源官方API。大概可以总结为两点:

  1. ThreadLocal提供get/set方法,可以访问属于当前线程的变量,也就是可以保证每个线程的变量不一样。
  2. ThreadLocal使用时通常定义为private static的。

从字面意思理解,可能会将ThreadLocal认为是本地线程,其实ThreadLocal并不是线程,而是线程Thread的局部变量。

ThreadLocal 是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其他线程来说无法获取到数据。

使用场景

  1. ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,每个线程都只能修改自己所拥有的副本, 而不会影响其他线程的副本,这样就让原本在并发情况下,线程不安全的情况变成了线程安全的情况。
  2. ThreadLocal 用作每个线程内需要独立保存信息的场景,供其他方法更方便得获取该信息,每个线程获取到的信息都可能是不一样的,前面执行的方法设置了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参。

使用方式

ThreadLocal 类接口很简单,只有 4 个方法,我们先来了解一下:

  • void set(Object value)
    设置当前线程的线程局部变量的值。
  • public Object get()
    该方法返回当前线程所对应的线程局部变量。
  • public void remove()
    将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是 JDK 5.0 新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
  • protected Object initialValue()
    返回该线程局部变量的初始值,该方法是一个 protected 的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第 1 次调用 get() 或 set(Object)时才执行,并且仅执行 1 次。ThreadLocal 中的缺省实现直接返回一 个 null。

使用起来出奇的简单,仅仅使用set()get()方法即可:

public class Main {

public static void main(String[] args) {
ThreadLocal<String> threadLocalOne = new ThreadLocal<>();
ThreadLocal<String> threadLocalTwo = new ThreadLocal<>();

new Thread(new Runnable() {
@Override
public void run() {
threadLocalOne.set("线程一的数据 --- threadLocalOne");
threadLocalTwo.set("线程一的数据 --- threadLocalTwo");
System.out.println(threadLocalOne.get());
System.out.println(threadLocalTwo.get());
}
}).start();

new Thread(new Runnable() {
@Override
public void run() {
System.out.println(threadLocalOne.get());
System.out.println(threadLocalTwo.get());
threadLocalOne.set("线程二的数据 --- threadLocalOne");
threadLocalTwo.set("线程二的数据 --- threadLocalTwo");
System.out.println(threadLocalOne.get());
System.out.println(threadLocalTwo.get());
}
}).start();
}
}

打印结果:

线程一的数据 --- threadLocalOne  
线程一的数据 --- threadLocalTwo
null
null
线程二的数据 --- threadLocalOne
线程二的数据 --- threadLocalTwo

源码分析

那么ThreadLocal是如何做到保证每个线程get出来的数据不一样的呢?
为什么通过 ThreadLocal 可以在不同的线程中维护一套数据的副本并且彼此互不干扰?
我们通过源码来看一下。

线程隔离

在上面的ThreadLocal的使用中,我们发现一个很有趣的事情,ThreadLocal在不同的线程,好像能够存储不同的数据:就好像ThreadLocal本身具有存储功能,到了不同线程,能够生成不同的'副本'存储数据一样

实际上,ThreadLocal到底是怎么做到的呢?

  • 来看下set()方法,看看到底怎么存数据的:此处涉及到ThreadLocalMap类型,暂且把他当成Map,详细的后面栏目分析

  • 其实这地方做了一个很有意思的操作:线程数据隔离的操作,是Thread类和ThreadLocal类相互配合做到的

  • 在下面的代码中可以看出来,在塞数据的时候,会获取执行该操作的当前线程

  • 拿到当前线程,取到threadLocals变量,然后仿佛以当前实例为key,数据value的形式往这个map里面塞值(有区别,set栏目再详细说)

  • 所以使用ThreadLocal在不同的线程中进行写操作,实际上数据都是绑定在当前线程的实例上,ThreadLocal只负责读写操作,并不负责保存数据,这就解释了,为什么ThreadLocal的set数据,只在操作的线程中有用

  • 大家有没有感觉这种思路有些巧妙!

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

//获取当前Thread的threadLocals变量
ThreadLocal.ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

//Thread类
public class Thread implements Runnable {
    ...
        
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    ...
}

Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系

先要搞清楚 Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系。我们用最直观、最容易理解的图画的方式来看看它们三者的关系:

我们看到最左下角的 Thread 1,这是一个线程,它的箭头指向了  ThreadLocalMap 1,其要表达的意思是,每个 Thread 对象中都持有一个 ThreadLocalMap 类型的成员变量,在这里 Thread 1 所拥有的成员变量就是 ThreadLocalMap 1。

而这个 ThreadLocalMap 自身类似于是一个 Map,里面会有一个个 key value 形式的键值对。那么我们就来看一下它的 key 和 value 分别是什么。可以看到这个表格的左侧是 ThreadLocal 1、ThreadLocal 2…… ThreadLocal n,能看出这里的 key 就是 ThreadLocal 的引用。

而在表格的右侧是一个一个的 value,这就是我们希望 ThreadLocal 存储的内容,例如 user 对象等。

这里需要重点看到它们的数量对应关系:一个 Thread 里面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。因为一个 Thread 是可以调用多个 ThreadLocal 的,所以 Thread 内部就采用了 ThreadLocalMap 这样 Map 的数据结构来存放 ThreadLocal 和 value。

通过这张图片,我们就可以搞清楚 Thread、 ThreadLocal 及 ThreadLocalMap 三者在宏观上的关系了。

get 方法

首先我们来看一下 get 方法,源码如下所示:

public T get() {

    //获取到当前线程

    Thread t = Thread.currentThread();

    //获取到当前线程内的 ThreadLocalMap 对象,每个线程内都有一个 ThreadLocalMap 对象

    ThreadLocalMap map = getMap(t);

    if (map != null) {

        //获取 ThreadLocalMap 中的 Entry 对象并拿到 Value

        ThreadLocalMap.Entry e = map.getEntry(this);

        if (e != null) {

            @SuppressWarnings("unchecked")

            T result = (T)e.value;

            return result;

        }

    }

    //如果线程内之前没创建过 ThreadLocalMap,就创建

    return setInitialValue();

}

这是 ThreadLocal 的 get 方法,可以看出它利用了 Thread.currentThread 来获取当前线程的引用,并且把这个引用传入到了 getMap 方法里面,来拿到当前线程的 ThreadLocalMap。

然后就是一个 if ( map != null ) 条件语句,那我们先来看看 if (map == null) 的情况,如果 map == null,则说明之前这个线程中没有创建过 ThreadLocalMap,于是就去调用 setInitialValue 来创建;如果 map != null,我们就应该通过 this 这个引用(也就是当前的 ThreadLocal 对象的引用)来获取它所对应的 Entry,同时再通过这个 Entry 拿到里面的 value,最终作为结果返回。

值得注意的是,这里的 ThreadLocalMap 是保存在线程 Thread 类中的,而不是保存在 ThreadLocal 中的。

分析:

  1. 一个线程对应一个 ThreadLocalMap,get() 就是当前线程获取自己的 ThreadLocalMap。
  2. 线程根据使用那一小块的 threadlocal,根据 ThreadLocal 对象作为 key,去获取存储于 ThreadLocalMap 中的值。

getMap 方法

下面我们来看一下 getMap 方法,源码如下所示:

ThreadLocalMap getMap(Thread t) {

    return t.threadLocals;

}

可以看到,这个方法很清楚地表明了 Thread 和 ThreadLocalMap 的关系,可以看出 ThreadLocalMap 是线程的一个成员变量。这个方法的作用就是获取到当前线程内的 ThreadLocalMap 对象,每个线程都有 ThreadLocalMap 对象,而这个对象的名字就叫作 threadLocals,初始值为 null,代码如下:

ThreadLocal.ThreadLocalMap threadLocals = null;

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);

}

set 方法的作用是把我们想要存储的 value 给保存进去。可以看出,首先,它还是需要获取到当前线程的引用,并且利用这个引用来获取到 ThreadLocalMap ;然后,如果 map == null 则去创建这个 map,而当 map != null 的时候就利用 map.set 方法,把 value 给 set 进去。

可以看出,map.set(this, value)  传入的这两个参数中,第一个参数是 this,就是当前 ThreadLocal 的引用,这也再次体现了,在 ThreadLocalMap 中,它的 key 的类型是 ThreadLocal;而第二个参数就是我们所传入的 value,这样一来就可以把这个键值对保存到 ThreadLocalMap 中去了。

我们发现在set方法中,会创建一个ThreadLocalMap,然后将要设置的值放在这个Map中,而当前这个ThreadLocal对象作为key;

然后再将这个ThreadLocalMap赋值给Thread的threadLocals里。如果去看Thread类的代码会发现,在Thread类中存在两个变量threadLocals和inheritableThreadLocals,它们的类型就是ThreadLocal.ThreadLocalMap。通过下图可以看出Thread,ThreadLocal,ThreadLocalMap三者的关系。

分析:

  1. 一个线程对应一个 ThreadLocalMap ,可以存储多个 ThreadLocal 对象。
  2. ThreadLocal 对象作为key、独享数据作为value。
  3. ThreadLocalMap 可参考 HashMap,在 ThreadMap 里面存在 Entry 数组也就是一个 Entry 一个键值对。

ThreadLocalMap 类,也就是 Thread.threadLocals

下面我们来看一下 ThreadLocalMap 这个类,下面这段代码截取自定义在 ThreadLocal 类中的 ThreadLocalMap 类:

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;
        }
    }
   private Entry[] table;

//...

}

ThreadLocalMap 类是每个线程 Thread 类里面的一个成员变量,其中最重要的就是截取出的这段代码中的 Entry 内部类。在 ThreadLocalMap 中会有一个 Entry 类型的数组,名字叫 table。我们可以把 Entry 理解为一个 map,其键值对为:

  • 键,当前的 ThreadLocal;
  • 值,实际需要存储的变量,比如 user 用户对象或者 simpleDateFormat 对象等。

ThreadLocalMap 既然类似于 Map,所以就和 HashMap 一样,也会有包括 set、get、rehash、resize 等一系列标准操作。但是,虽然思路和 HashMap 是类似的,但是具体实现会有一些不同。

比如其中一个不同点就是,我们知道 HashMap 在面对 hash 冲突的时候,采用的是拉链法。它会先把对象 hash 到一个对应的格子中,如果有冲突就用链表的形式往下链,如下图所示:

但是 ThreadLocalMap 解决 hash 冲突的方式是不一样的,它采用的是线性探测法。如果发生冲突,并不会用链表的形式往下链,而是会继续寻找下一个空的格子。这是 ThreadLocalMap 和 HashMap 在处理冲突时不一样的点。

使用场景

通过上面的内容基本可以掌握ThreadLocal的基本用法,那么ThreadLocal主要在什么场景中使用呢。

ThreadLocal的作用通过以上了解我们知道主要是用来做线程间数据隔离。那么在什么场景下能用到线程隔离呢?

首先想到的就是SimpleDateFormat这个工具类,它不是线程安全的,可以通过ThreadLocal在每个线程中放一份,保证线程安全。

还有比如说用户登录的session,或者token数据,只数据当前会话线程,也可以通过ThreadLocal存储。

再比如在某些场景下,上下文数据在不同方法之间调用,传递起来非常麻烦,可以通过ThreadLocal存放,只需要在需要用到的地方获取就可以。

除了这些场景,在某些框架源码中也会使用到,比如Spring中的事务也主要是通过ThreadLocal和面向切面编程AOP实现的,感兴趣的同学可以查看源码了解。

内存泄漏

什么是内存泄漏

内存泄漏指的是,当某一个对象不再有用的时候,占用的内存却不能被回收,这就叫作内存泄漏

因为通常情况下,如果一个对象不再有用,那么我们的垃圾回收器 GC,就应该把这部分内存给清理掉。这样的话,就可以让这部分内存后续重新分配到其他的地方去使用;否则,如果对象没有用,但一直不能被回收,这样的垃圾对象如果积累的越来越多,则会导致我们可用的内存越来越少,最后发生内存不够用的 OOM 错误。

下面我们来分析一下,在 ThreadLocal 中这样的内存泄漏是如何发生的。

Key 的泄漏

在上面我们分析了 ThreadLocal 的内部结构,知道了每一个 Thread 都有一个 ThreadLocal.ThreadLocalMap 这样的类型变量,该变量的名字叫作 threadLocals。线程在访问了 ThreadLocal 之后,都会在它的 ThreadLocalMap 里面的 Entry 中去维护该 ThreadLocal 变量与具体实例的映射。

我们可能会在业务代码中执行了 ThreadLocal instance = null 操作,想清理掉这个 ThreadLocal 实例,但是假设我们在 ThreadLocalMap 的 Entry 中强引用了 ThreadLocal 实例,那么,虽然在业务代码中把 ThreadLocal 实例置为了 null,但是在 Thread 类中依然有这个引用链的存在。

GC 在垃圾回收的时候会进行可达性分析,它会发现这个 ThreadLocal 对象依然是可达的,所以对于这个 ThreadLocal 对象不会进行垃圾回收,这样的话就造成了内存泄漏的情况。

JDK 开发者考虑到了这一点,所以 ThreadLocalMap 中的 Entry 继承了 WeakReference 弱引用,代码如下所示:

static class Entry extends WeakReference<ThreadLocal<?>> {

    /** The value associated with this ThreadLocal. */

    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

可以看到,这个 Entry 是 extends WeakReference。弱引用的特点是,如果这个对象只被弱引用关联,而没有任何强引用关联,那么这个对象就可以被回收,所以弱引用不会阻止 GC。因此,这个弱引用的机制就避免了 ThreadLocal 的内存泄露问题。

这就是为什么 Entry 的 key 要使用弱引用的原因。

  1. Key 使用强引用:也就是上述说的情况,引用 ThreadLocal 的对象被回收了,ThreadLocal 的引用 ThreadLocalMap 的 Key 为强引用并没有被回收,如果不手动回收的话,ThreadLocal 将不会回收那么将导致内存泄漏。
  2. Key 使用弱引用:引用的 ThreadLocal 的对象被回收了,「ThreadLocal的引用 ThreadLocalMap 的 Key 为弱引用,如果内存回收,那么将ThreadLocalMap 的 Key 将会被回收,ThreadLocal 也将被回收。value 在ThreadLocalMap 调用 get、set、remove 的时候就会被清除」
  3. 比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:「弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除」

Value 的泄漏

可是,如果我们继续研究的话会发现,虽然 ThreadLocalMap 的每个 Entry 都是一个对 key 的弱引用,但是这个 Entry 包含了一个对 value 的强引用,还是刚才那段代码:

static class Entry extends WeakReference<ThreadLocal<?>> {

    /** The value associated with this ThreadLocal. */

    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

可以看到,value = v 这行代码就代表了强引用的发生。

正常情况下,当线程终止,key 所对应的 value 是可以被正常垃圾回收的,因为没有任何强引用存在了。但是有时线程的生命周期是很长的,如果线程迟迟不会终止,那么可能 ThreadLocal 以及它所对应的 value 早就不再有用了。在这种情况下,我们应该保证它们都能够被正常的回收。

为了更好地分析这个问题,我们用下面这张图来看一下具体的引用链路(实线代表强引用,虚线代表弱引用):

可以看到,左侧是引用栈,栈里面有一个 ThreadLocal 的引用和一个线程的引用,右侧是我们的堆,在堆中是对象的实例。

我们重点看一下下面这条链路:Thread Ref → Current Thread → ThreadLocalMap → Entry → Value → 可能泄漏的value实例

这条链路是随着线程的存在而一直存在的,如果线程执行耗时任务而不停止,那么当垃圾回收进行可达性分析的时候,这个 Value 就是可达的,所以不会被回收。但是与此同时可能我们已经完成了业务逻辑处理,不再需要这个 Value 了,此时也就发生了内存泄漏问题。

JDK 同样也考虑到了这个问题,在执行 ThreadLocal 的 set、remove、rehash 等方法时,它都会扫描 key 为 null 的 Entry,如果发现某个 Entry 的 key 为 null,则代表它所对应的 value 也没有作用了,所以它就会把对应的 value 置为 null,这样,value 对象就可以被正常回收了。

但是假设 ThreadLocal 已经不被使用了,那么实际上 set、remove、rehash 方法也不会被调用,与此同时,如果这个线程又一直存活、不终止的话,那么刚才的那个调用链就一直存在,也就导致了 value 的内存泄漏。

如何避免内存泄露

分析完这个问题之后,该如何解决呢?解决方法就是我们本课时的标题:调用 ThreadLocal 的 remove 方法。调用这个方法就可以删除对应的 value 对象,可以避免内存泄漏。

我们来看一下 remove 方法的源码:

public void remove() {

    ThreadLocalMap m = getMap(Thread.currentThread());

    if (m != null)
        m.remove(this);
}

可以看出,它是先获取到 ThreadLocalMap 这个引用的,并且调用了它的 remove 方法。这里的 remove 方法可以把 key 所对应的 value 给清理掉,这样一来,value 就可以被 GC 回收了。

所以,在使用完了 ThreadLocal 之后,我们应该手动去调用它的 remove 方法,目的是防止内存泄漏的发生。

总结

  1. ThreadLocalMap 内部 Entrykey 使用的是对 ThreadLocal 对象的弱引用,这为避免内存泄露是一个进步,因为如果是强引用,那么即使其他地方没有对 ThreadLocal 对象的引用,ThreadLocalMap 中的 ThreadLocal 对象还是不会被回收,而如果是弱引用则这时候 ThreadLocal 引用是会被回收掉的。
  2. 但是对于的 value 还是不能被回收,这时候 ThreadLocalMap 里面就会存在 keynull 但是 value 不为 nullentry 项,虽然 ThreadLocalMap 提供了 set,get,remove 方法在一些时机下会对这些 Entry 项进行清理,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露,所以在使用完毕后即使调用 remove 方法才是解决内存泄露的最好办法。
  3. 线程池里面设置了 ThreadLocal 变量一定要记得及时清理,因为线程池里面的核心线程是一直存在的,如果不清理,那么线程池的核心线程的 threadLocals 变量一直会持有 ThreadLocal 变量。

参考感谢
https://mp.weixin.qq.com/s/MpImTSSTG5qeuXMKATYdkQ
https://kaiwu.lagou.com/course/courseInfo.htm?courseId=16#/detail/pc?id=285
https://mp.weixin.qq.com/s/7XEIqTIv8W8CNNYnWqe2rw