使用 Java 中的 ThreadLocal 类可以创建只能由同一个线程读写的变量。也就是说,即使两个线程在执行相同的代码,并且代码中都有对同一个 ThreadLocal 变量的引用,那么这两个线程也不会看到对方的 ThreadLocal 变量。因此,Java 中的 ThreadLocal 类提供了一种简单的方式来使代码中的线程变得更加安全。

概述

在多线程并发的情况下,需要考虑到线程安全的问题,例如同步、互斥等。一般采用以下三种方式来实现线程安全:

  • 互斥同步:通过加锁来实现对临界资源的访问,如 synchronized 和 Lock;
  • 非阻塞同步:上面的互斥同步属于悲观锁机制,而非阻塞同步属于乐观锁机制,典型的实现方式是 CAS;
  • 无同步:要保证线程安全,并不一定就需要同步。同步只是保证共享数据争用时正确的手段。如果一个方法本来就不涉及共享数据,那它就不需要任何同步措施去保证正确性。因此就引出了 ThreadLocal 的概念。

从字面上看,ThreadLocal 就是线程本地线程局部的意思,但该类提供了线程本地变量(thread-local variables),这种变量与普通变量的不同之处在于:每个访问 ThreadLocal 变量的线程都有自己、独立的变量副本,并且每个线程都可以独立的改变自己的副本,不会与其它线程中的副本发生冲突

也就是说,如果想要将某些共享数据的可见范围限制在同一个线程之内,并且想要达到无需同步也能保证线程之间不会出现数据争用的问题,此时就可以使用 ThreadLocal。即如果某个变量想要被某个线程独享,则可以使用 ThreadLocal 来实现线程本地的功能。

从另一方面想,只要线程还存活,并且被 ThreadLocal 变量可以访问,则每个线程都会有一个引用到该线程本地变量的副本。当线程结束执行后,它的所有的线程本地实例的副本就会被垃圾回收。

JDK1.8 中的 ThreadLocal 定义如下所示:

1
public class ThreadLocal<T> {...}

从定义来看,ThreadLocal 在声明变量时可以自定义变量所属的类型,在其附带的说明文档中,需要注意以下几点:

  • 每个线程都有关于该 ThreadLocal 变量的私有值,即每个线程都有一个独立于其它线程的变量值,并且对其它线程是不可见的;
  • 该 ThreadLocal 变量的初始值也是相互独立的,即 ThreadLocal 可以给定一个初始值,这样每个线程就会获得这个初始值的一个拷贝,并且就每个线程对这个值的改变这一动作来说,对其它线程也是不可见的;
  • ThreadLocal 的作用就是将类的状态和线程关联起来,此时可以通过声明一个 private static ThreadLocal 实例进行实现。

综上所述,这里大体概括一下 ThreadLocal 的实现思路:

  • 在 Thrad 类中有一个类型为 ThreadLocal.ThreadLocalMap 的实例变量 threadLocals,即每个线程都拥有一个自己的 ThreadLocalMap
  • ThreadLocalMap 位于 ThreadLocal 类中,并且有自己具体的实现方式,可以将它的 key 视为 ThreadLocal,value 视为代码中放入的值,但实际上 key 并不是 ThreadLocal 本身,而是它的一个弱引用
  • 每个线程在往某个 ThreadLocal 里放入值的时候,都会往自己的 ThreadLocalMap 里存,读的时候也是以某个 ThreadLocal 作为引用,在自己的 map 里找对应的 key,从而实现线程隔离

基本使用

下面通过一个具体的实例,来说明 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
public class Test {
    private static int a = 10;
    private static ThreadLocal local;

    public static void main(String[] args) {

        Thread a = new Thread(new ThreadA());
        a.start();

        ThreadB b = new ThreadB();
        b.start();
    }

    static class ThreadA implements Runnable {

        @Override
        public void run() {
            local = new ThreadLocal();
            System.out.println("set 方法执行前: " + local.get() + " " + Thread.currentThread().getName());
            local.set(a + 10);
            System.out.println("set 方法执行后: " + local.get() + " " + Thread.currentThread().getName());
            System.out.println("remove 方法执行前: " + local.get() + " " + Thread.currentThread().getName());
            local.remove();
            System.out.println("remove 方法执行后: " + local.get() + " " + Thread.currentThread().getName());
        }
    }

    static class ThreadB extends Thread {

        @Override
        public void run() {
            System.out.println("另一个线程中的值: " + local.get() + " " + Thread.currentThread().getName());
        }
    }
}

输出结果(顺序不唯一)如下所示:

1
2
3
4
5
set 方法执行前: null Thread-0
set 方法执行后: 20 Thread-0
remove 方法执行前: 20 Thread-0
remove 方法执行后: null Thread-0
另一个线程中的值: null Thread-1

实例中,ThreadA 类的 run 方法中初始化了 ThreadLocal 变量,调用 set(a + 10) 方法将其设置成了 20,随后执行了 remove 方法,此时该变量的值就为 null 了。然而,当 ThreadB 线程想要获得该值时,由于每个线程都有自己的一份变量,因此 ThreadB 线程获得的值也是 null,这个 null 值与 ThreadA 中的 remove 方法是没有关系的,也就是说,ThreadA 的 remove 只是删除了它自己的值,而不能删除其它线程中的值。

我们先从总体再到局部,通过这种方式来分析 ThreadLocal 类。从总体上来说,ThreadLocal 类中有一个静态内部类 ThreadLocalMap,这个 ThreadLocalMap 是自定义的哈希表,用于维护线程本地值。哈希表里面的每个位置就对应一个 Entry 对象,而每个 Entry 对象中存放了键值对,其中 key 表示的是 ThreadLocal 实例,而 value 表示的是该 ThreadLocal 所对应的值。

下面先看 内部类 ThreadLocalMap。

ThreadLocalMap 类

ThreadLocalMap 是 ThreadLocal 的重点,它专门为 ThreadLocal 定制了一种高效的实现,并且自带一种基于弱引用的垃圾回收机制

Entry

ThreadLocalMap 中的 map 是概念上的 map,因此它也会有 key 和value。但需要注意的是:为了便于理解,我们可以将 key 视为 ThreadLocal,将 value 视为 实际放入的值。但实际上,ThreadLocal 中存放的是 ThreadLocal 的弱引用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static class Entry extends WeakReference<ThreadLocal<?>> {
    // 这里的 value 与 ThreadLocal 有关,
    // 即 value 就是往 ThreadLocal 实际塞入的值
    Object value;

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

Entry 是 ThreadLocalMap 中定义的节点,它继承了 WeakReference 类,然后定义了一个 Object 类型的 value,用于存放塞到 ThreadLocal 里的值。

需要注意的是,为什么 Entry 要继承弱引用

在垃圾回收机制中,JVM 针对不同对象存在着不同的引用形式,具体在 《JVM 对象存活和垃圾收集算法》中提到过四种引用方式。如果这里使用普通的 key-value 形式来定义存储结构,实际上就会造成节点的生命周期与线程之间的强绑定。只要线程没有被销毁,那么节点在 GC 分析的时候就一直处于引用状态,没办法被回收,同时程序本身也无法判断是否可以清理这个节点。

在四种引用类型(强引用、软引用、弱引用、虚引用)中,弱引用比软引用还要弱一些,如果一个对象没有被强引用,那么一般活不过下次 GC。也就是说,只要一个对象被强引用绑定,那么垃圾收集器就不会回收掉该对象。而一个对象被软引用、弱引用、虚引用绑定后,会被下次垃圾收集器进行回收。当某个 ThreadLocal 已经没有绑定强引用的时候,随着它被垃圾收集器回收,在 ThreadLocalMap 里对应的 Entry 的键值就会失效,这位 ThreadLocalMap 本身的垃圾清理提供了便利

成员变量

静态内部类 ThreadLocalMap 中定义了用于存储键值对的 Entry 数组,即 table,它的长度必须是 2 的次幂。既然是哈希表,肯定会涉及到初始化容量以及扩容的概念,如下所示:

1
2
3
4
5
6
7
private static final int INITIAL_CAPACITY = 16;

private Entry[] table;

private int size = 0;

private int threshold;
  • INITIAL_CAPACITY:表示哈希表的初始化容量为 16,必须保证是 2 的次幂;
  • table:表示用于存储键值对的 Entry 数组,它的长度也必须是 2 的次幂;
  • size:表示当前数组中 entry 的个数;
  • threshold:表示当数组中的元素超过了该值时,则进行扩容,再进行扩容时,重新分配 Entry 数组大小的阈值,默认为 0;

成员方法

可以通过 setThreshold() 方法来设置负载因子的大小,负载因子的概念在 HashMap 中已经很熟悉了。然后 ThreadLocal 通过两个方法用来获得上一个或下一个索引。需要注意的是,ThreadLocalMap 使用线性探测法来解决散列的冲突,所以实际上 Entry 数组在程序逻辑上是以环状数组存在的。数组中元素 Entry 的 key 存储的是某个 ThreadLocal 对象,即指向该 ThreadLocal 对象的弱引用,value 表示往 ThreadLocal 变量实际塞入的值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 设置 resize 阈值以维护最坏情况下的 2/3 的负载因子
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

// 获取下一个索引
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

// 获取上一个索引
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

构造函数

该构造函数在初始化的时候,会构造一个包含 firstKey 和 firstValue 的 map。ThreadLocalMap 是通过惰性的方式构造的,所以只有当至少要往里面放一个元素的时候才会构造它。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 构造 table 数组
    table = new Entry[INITIAL_CAPACITY];
    // 需要注意的是,以下的计算方式在 HashMap 中也有存在,
    // 表示通过计算 firstKey 的 threadLocalHashCode 与初始容量 16 进行模运算,得到 firstKey 的哈希值
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 将 ThreadLocal 实例和要保存的值存到对应的位置上
    table[i] = new Entry(firstKey, firstValue);
    // 设置数组的大小
    size = 1;
    // 设置扩容阈值
    setThreshold(INITIAL_CAPACITY);
}

在计算 firstKey 的哈希值的时候,使用了hashCode & (size - 1)的方法,这相当于取模运算hashCode % size的一个更高效的实现,HashMap 中也采用了类似的方法。正是因为这种方法,所以我们要求 size 必须是 2 的次幂,因为这样可以使得 hash 发生冲突的次数减小。

getEntry()

该方法的作用是返回与 key 关联的 entry 对象,也就是获取 map 中某个 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
private Entry getEntry(ThreadLocal<?> key) {
    // 根据 key 获取索引
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 对应的 entry 存在且未失效,并且弱引用指向的 ThreadLocal 就是这个 key,则将其返回
    if (e != null && e.get() == key)
        return e;
    else
        // 否则如果在散列槽中找不到对应的 key 时,采用线性探测的方式往后找
        return getEntryAfterMiss(key, i, e);
}

// 当调用 getEntry() 方法未命中时,则调用下列方法
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    // 使用线性探测法不断向后探测,直到遇到空的 entry
    while (e != null) {
        ThreadLocal<?> k = e.get();
        // 找到目标就返回
        if (k == key)
            return e;
        if (k == null)
            // 该 entry 对应的 ThreadLocal 已经被回收了,调用以下方法来清理无效的 entry
            expungeStaleEntry(i);
        else
            // 继续向后探测
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

因此,ThreadLocal 在读一个值的时候,首先需要根据 key 的 threadLocalHashCode 对数组容量取模得到 index,如果 index 对应的位置就是要读的 ThreadLocal,,则直接返回结果。否则调用 getEntryAfterMiss() 方法进行线性探测,在探测的过程中每次碰到无效的位置,则调用 expungeStaleEntry() 进行清理。如果找到了 key,则返回对应的 entry,没有找到 key,则返回 null。

set()

set() 的作用是设置与 key 对应的 value,这里的 key 就是线程本地对象(thread local object),value 就是准备要设置的值,如下所示:

 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
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    // 线性探测
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        // 找到对应的 entry 并设置为 value
        if (k == key) {
            e.value = value;
            return;
        }
        // 替换失效的 entry
        if (k == null) {
            // 用给定 key 所对应的 entry 去替换在探测过程中遇到的第一个旧的 entry,
            // i 表示在查抄 key 时遇到的第一个旧的 entry 的索引
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    // cleanSomeSlots() 表示启发式地扫描某些位置,寻找旧的 entry,
    // 当添加了一个新的元素或者另一个旧的元素被删除时,此方法会被调用。
    // 在扫描的过程中,如果有任何 entry 被删除的话,则返回 true
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

整体流程为:

  • 在探测过程中,solt(即数组中的每个位置)都不是无效的,并且能够顺利的找到 key 所在的 solt,则直接通过 set() 替换即可。
  • 如果在探测过程中,发现 solt 存在无效的情况,则调用 replaceStaleEntry() 把 key 和 value 放在这个 solt 上;
  • 如果在探测的过程中没有发现 key,则在连续末尾的后一个空位置上放置一个 entry,然后做一次启发式的清理,即 cleanSomeSlots()。如果没有将 key 清理掉,并且当前 table 的大小已经超过了阈值,则需要进行 rehash() 操作。

需要注意的是,由于需要保证 table 的容量为 2 的次幂,因此 rehash() 里面的 resize() 方法在扩容的时候将会扩大 2 倍,即newLen = oldLen * 2

remove()

删除 key 所对应的 entry,也就是从 map 中删除 ThreadLocal。即直接在 table 数组中找 key,如果找到了,那就把弱引用断开,然后做一次段清理。如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            // 显式地断开弱引用
            e.clear();
            // 进行段清理
            expungeStaleEntry(i);
            return;
        }
    }
}

ThreadLocal 类

get()

get 方法将会返回当前线程的线程本地变量值,如果对于当前线程来说没有对应的值,则会调用 setInitialValue() 方法并返回此方法的值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public T get() {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取当前线程的成员变量 threadLocals
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 从当前线程的 ThreadLocalMap 中获得该线程本地变量对应的 entry 
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            // 获取对应的值并进行返回
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

从上面可以看到,再进行 get 时,如果从当前线程获取到的成员变量不为空的话,则会调用 map.getEntry(this) 去找到对应的值并进行返回。而如果 map 为空,则会调用 setInitialValue() 方法来设置一个初始化值,下面看一下这个方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private T setInitialValue() {
    // 默认的返回值就是 null
    T value = initialValue();
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 得到当前线程 ThreadLocalMap 类型的 threadLocals 
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 该 map 的键是当前 ThreadLocal 对象
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}


protected T initialValue() {
    return null;
}


void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

可以看出,如果 map 为空,则会对 threadLocals 进行初始化,以当前 ThreadLocal 变量为 key,以 ThreadLocal 要保存的值为 value,存储到 threadLocals。get 方法会获得当前线程的 threadLocals,然后以该 ThreadLocal 对象作为键从而取得其对应的值,也就是 ThreadLocal 对象中所存储的值。

然后是 ThreadLocalMap 中的 getEntry 方法,以及 getEntryAfterMiss 方法:

 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
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}


private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

get 方法的总体流程:

    1. 首先获取当前线程;
    1. 尝试去当前线程中获得它的 ThreadLocal.ThreadLocalMap;
    1. 判断当前线程中是否有 ThreadLocal.ThreadLocalMap:
    • 3.1 如果有,则根据当前 ThreadLocal 的 threadLocalHashCode 取模去 table 中取值。有值的话就返回,没有的话就给模加 1 继续找;
    • 3.2 如果没有,则调用 setInitialValue 方法给当前线程 ThreadLocal.ThreadLocalMap 设置一个初始值。

set()

set() 方法的大体流程是:

  • 首先获得当前的线程;
  • 然后获得当前线程中的 ThreadLocal.ThreadLocalMap;
  • 判断该 ThreadLocal.ThreadLocalMap 是否存在,如果存在则设置一个值,如果不存在就给线程创建一个 ThreadLocal.ThreadLocalMap。

源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}


ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

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

再看一下这里的 createMap 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}


private final int threadLocalHashCode = nextHashCode();


private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}


private static AtomicInteger nextHashCode = new AtomicInteger();

这里需要注意的是,虽然 Entry 中存储的是键值对,但当 table 中已经有了数据的时候,则采用的是开放地址法,也就是会采用加 1 的方式来到下一个位置继续进行判断。

再来看一下静态内部类 ThreadLocalMap 中的 set 方法:

 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
private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

// 开放地址法
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

set 方法小结:

  • 首先对 ThreadLocal 里面的 threadLocalHashCode 取模得到一个 table 中的位置;
  • 如果这个位置上有数据,则获取该位置上的 ThreadLocal:
    • 判断该位置上的 ThreadLocal 和当前的 ThreadLocal 是不是同一个,如果是的话就覆盖,然后返回;
    • 如果不是同一个的话,再判断一下该位置上的 ThreadLocal 是不是为空。因为 Entry 是 ThreadLocal 的弱引用,即 static class Entry extends WeakReference<ThreadLocal> 有可能这个 ThreadLocal 被垃圾回收了,这个时候把新设置的 value 替换到当前位置上,然后返回。
    • 如果上面都没有返回,则使用开放地址法进行加 1 操作,即来到下一个 table 的位置。在加 1 操作之后需要判断新的 table 位置是不是为空,如果为空则再次进行加 1 操作,一直找到一个 table 上不是空的为止,然后放入一个 value。

remove()

1
2
3
4
5
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

在 remove 方法中,同样也是获取到当前线程的 ThreadLocal.ThreadLocalMap,如果有的话则找到对应的 Entry 然后删除即可。

ThreadLocal 小结

    1. 线程里面当前的 ThreadLocal.ThreadLocalMap 是通过开放地址法实现的;
    1. 每次 set 的时候往线程里面的 ThreadLocal.ThreadLocalMap 中的 table 数组中某一个位置上放一个值,该位置由 ThreadLocal 中的 threadLocaltHashCode 取模得到。如果该位置上已经有数据了,则往后一直找,从而找到没有数据的一个位置;
    1. 每次 get 的时候也是根据 ThreadLocal 中的 threadLocaltHashCode 取模得到 ThreadLocal.ThreadLocalMap 中的 table 数组中对应的位置,如果有数据则返回该数据,没有的话就来到下一个位置找;
    1. 如果想要往 ThreadLocal 中存放不同类型的数据,则需要使用泛型,如 ThreadLocal、ThreadLocal 等。

内存泄漏问题

ThreadLocal 类解决的是变量在不同线程间的通信问题。常见的使用场景,如多线程场景下每个线程需要单独的变量实例、数据库连接问题、存储用户的 Session 等。

需要注意的是,如果使用 ThreadLocal 不当的话,则有可能出现内存泄漏的问题。其中内存泄漏指的是由于疏忽或者错误造成程序未能释放已经不再使用的内存,这些不再使用的内存会占用着内存空间,从而导致内存泄漏。也就是说,内存泄漏并非指的是内存在物理上的消失,而是应用程序分配某段内存后,由于错误设计,导致在释放该内存之前就失去了对该段内存的控制,从而造成了内存的浪费。

ThreadLocal 造成内存泄漏的原因分为两种场景:主线程仍然对 ThreadLocal 具有引用和主线程不存在对 ThreadLocal 的引用。第一种场景因为主线程仍然在运行,所以还是具有对 ThreadLocal 的引用,那么 ThreadLocal 变量的引用和对应的 value 是不会被回收的。第二种场景是虽然主线程不存在对 ThreadLocal 的引用,并且该引用属于弱引用,所以在 GC 的时候会被回收,但是对应的 value 不是弱引用,不会被回收,因此会造成内存泄漏的问题。

如何解决内存泄漏问题

既然 ThreadLocalMap 中存在 key 为 null 的 Entry 对象,从而导致内存泄漏,那么只要把这些 Entry 都给删除掉就可以了。因此,可以调用 remove 方法,根据 key 删除掉对应的 Entry 即可。上面提到的 remove 方法里面调用了 ThreadLocalMap 中的 remove 方法,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

参考