ThreadLocal概念

ThreadLocal,即线程变量,是以一个ThreadLocal对象为key、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。

ThreadLocal使用场景

场景1,ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全

ThreadLocal解决线程安全问题的时候,相比于使用“锁”而言,换了一个思路,把资源变成了各线程独享的资源,非常巧妙地避免了同步操作。具体而言,它可以在initialValue中new出自己线程独享的资源,而多个线程之间,它们所访问的对象本身是不共享的,自然就不存在任何并发问题。这是 ThreadLocal 解决并发问题的最主要思路。

场景2,ThreadLocal用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过ThreadLocal直接获取到,避免了传参,类似于全局变量的概念。

源码

先看下,Thread、ThreadLocal及ThreadLocalMap三者之间的关系:

ThreadLocal_1.png

我们看到最左下角的 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。

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

它利用了Thread.currentThread来获取当前线程的引用,并且把这个引用传入到了getMap方法里面,来拿到当前线程的ThreadLocalMap。
值得注意的是,这里的ThreadLocalMap是保存在线程Thread类中的,而不是保存在ThreadLocal中的。

getMap方法

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

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

ThreadLocal.ThreadLocalMap threadLocals = null;

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

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

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,值则为实际需要存储的变量。

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

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

ThreadLocal_2.png

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

内存泄露问题

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

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

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

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_3.png

可以看到,左侧是引用栈,栈里面有一个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);
}

这样value就可以被GC回收了。

Last modification:April 30, 2020
如果觉得我的文章对你有用,请随意赞赏