前言

ThreadLocal的两种使用场景 中我们初识了ThreadLocal及使用场景。

本文通过四个问题来深入剖析ThreadLocal的源码,从而能够对ThreadLocal有更深层次的理解。

问题一: ThreadLocal 是不是用来解决共享资源的多线程访问的?

答案:不是。

我们把 ThreadLocal的两种使用场景 一节中的例子进行改造,将initialValue()方法中的new SimpleDateFormat()方法改成获取ThreadLocalDemo02类的静态变量SimpleDateFormat,加上static 关键字,并且把这个静态对象放到 ThreadLocal 中去存储,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadLocalDemo02 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
static SimpleDateFormat simpleDateFormat=new SimpleDateFormat("mm:ss");

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalDemo02().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}

public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}

class ThreadSafeFormatter {

public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {

@Override
protected SimpleDateFormat initialValue() {
return ThreadLocalDemo02.simpleDateFormat;
}
};
}

测试结果:

依然有并发问题,说明SimpleDateFormat是被共享的,那么此时的ThreadLocal就没有什么作用,它并没有解决并发问题,也没有起到任何作用。

对于这种共享的变量,如果想要保证它的线程安全,应该用其他的方法,比如说可以使用 synchronized 或者是加锁等其他的方法来解决线程安全问题,而不是使用 ThreadLocal,因为这不是 ThreadLocal 应该使用的场景。

问题二: ThreadLocal 和 synchronized 是什么关系

  1. 第一点,两者都可以解决线程安全问题

    解决线程安全问题的方式不同。ThreadLocal解决线程安全的时候,是通过把对象给每一个线程复制一个独享的副本,各个线程的副本之间互不干扰,从而解决了线程安全问题。而synchronized是通过对唯一的一个对象的访问限制来实现的。

    ThreadLocal的两种使用场景 中我们通过ThreadLocal和synchronized来解决线程安全问题,对于SimpleDateFormat对象来说,用ThreadLocal来解决的话,每个线程都有一个SimpleDateFormat对象,各个线程之间并行处理,互不干扰。用synchronized来解决的话,所有的线程共用一个SimpleDateFormat对象,每个线程要想访问该SimpleDateFormat对象,就得获取该对象的锁,同一时刻只能有一个线程拿到锁,也就是同一时刻只能有一个线程操作SimpleDateFormat对象,这样就相当于串行处理,虽然解决了线程安全问题,但影响了并行的效率。

  2. 第二点,ThreadLocal比synchronized有更多的使用场景

    比如当 ThreadLocal 用于让多个类能更方便地拿到我们希望给每个线程独立保存这个信息的场景下时(比如每个线程都会对应一个用户信息,也就是 user 对象),在这种场景下,ThreadLocal 侧重的是避免传参,所以此时 ThreadLocal 和 synchronized 是两个不同维度的工具。

问题三: ThreadLocal的底层是如何实现的?

先看一张图,如下:

通过此图我们分析 Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系,

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

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

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

简而言之,一个thread内部对应唯一一个threadlocalMap,threadlocalMap里面可以存放许多个键值对,其中每一个键就是ThreadLocal的引用,键对应的值就是我们希望threadlocal存储的内容,比如User对象等

分析源码:

分析threadlocal的get方法,如下:

值得注意的是,ThreadLocalMap是保存在thread对象中的,而不是ThreadLocal中:

可以看到ThreadLocalMap 是线程的一个成员变量。

getMap()方法的作用就是获取到当前线程内的 ThreadLocalMap 对象,每个线程都有 ThreadLocalMap 对象,而这个对象的名字就叫作 threadLocals,初始值为 null。

再来分析下set方法的源码,如下图所示:

我们来看下ThreadLocalMap 类的源码,如下:

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

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

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

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

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

问题四:为什么用完 ThreadLocal 之后都要求调用 remove 方法?

在剖析这个问题之前,我们先了解内存泄漏有关的知识。

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

下面我们来分析一下,在 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 弱引用,源码如下所示:

1
2
3
4
5
6
7
8
9
 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 要使用弱引用的原因。

Value 的泄漏

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

1
2
3
4
5
6
7
8
9
10
11
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 对象,可以避免内存泄漏。

该方法源码如下:

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

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

总结

本文我们从四个问题出发,分析ThreadLocal的底层实现,包含内部的ThreadLocalMap结构等,以及为何调用remove()方法能够避免内存泄漏。