ThreadLocal 源码深入解析

2022-06-27 16:50:42 浏览数 (1)

1. 概述

在 java 中,ThreadLocal 作为线程间共享数据和缓存数据的重要工具是十分常用的,而它的设计更让人觉得巧妙。

本篇日志中,就让我们来深入源码,对 ThreadLocal 一探究竟。

2. 简介

ThreadLocal类用来提供线程内部的局部变量。这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。也就是说,ThreadLocal 实现了在堆上分配线程内部可见的空间,那么,这究竟是如何实现的呢?

3. ThreadLocal 用法

我们首先来看看 ThreadLocal 提供了哪些方法供我们使用。

  • 静态初始化方法 -- public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)
  • 值获取方法 -- public T get()
  • 值设置方法 -- public void set(T value)
  • 显式清除方法 -- public void remove()接下来,我们就通过以上方法的源码来一窥 ThreadLocal 的实现。

3.1. 值设置方法 -- public void set(T value)

我们首先来看看 ThreadLocal 最重要的 set 方法。

  • set 方法
代码语言:javascript复制
public void set(T value) {  
        Thread t = Thread.currentThread();  
        ThreadLocalMap map = getMap(t);  
        if (map != null)  
                map.set(this, value);  
        else  
                createMap(t, value);  
}  

这个方法看上去很简单,调用 getMap 来获取 ThreadLocalMap 对象,并设置值或创建 ThreadLocalMap 对象。那么 getMap 方法做了什么呢?

  • getMap 方法
代码语言:javascript复制
ThreadLocalMap getMap(Thread t) {  
        return t.threadLocals;  
}  

可以看到,线程对象 Thread 有一个 friendly 的成员变量 ThreadLocal.ThreadLocalMap threadLocals = null; 这使得只有同一个包内的 ThreadLocal 代码中可以使用这个变量。正是这个变量的存在,为 Thread 保存了堆空间上线程内部的局部变量。由于这个变量存储在线程对象中,因此 ThreadLocal 的 set 方法完全不需要考虑线程同步问题。那么 ThreadLocalMap 又是什么呢?顾名思义,他就是一个用来在线程空间中保存所有堆内存上 ThreadLocal 变量和值得缓存,具体的代码我们稍后阅读,先来看一下整个内存结构的模型图。

3.2. 值的获取 -- get 方法

代码语言:javascript复制
public T get() {  
        Thread t = Thread.currentThread();  
        ThreadLocalMap map = getMap(t);  
        if (map != null) {  
                ThreadLocalMap.Entry e = map.getEntry(this);  
                if (e != null) {  
                        @SuppressWarnings("unchecked")  
                        T result = (T)e.value;  
                        return result;  
                }  
    }  
        return setInitialValue();  
}  

这个方法比较简单,在当前线程的 ThreadLocalMap 中查找当前对象为 key 的值,如果不存在,则调用值初始化方法返回默认值。

3.3. 值初始化方法 -- setInitialValue

代码语言:javascript复制
private T setInitialValue() {  
        T value = initialValue();  
        Thread t = Thread.currentThread();  
        ThreadLocalMap map = getMap(t);  
        if (map != null)  
                map.set(this, value);  
        else  
                createMap(t, value);  
        return value;  
}  

4. ThreadLocalMap

4.1. ThreadLocalMap.Entry

我们曾经读过 HashMap 的源码。java HashMap 源码解析我们知道,HashMap 是一个 Entry 的集合,而 ThreadLocalMap 的核心同样也是一个 Entry 的集合。

代码语言:javascript复制
static class Entry extends WeakReference<ThreadLocal<?>> {  
        /** The value associated with this ThreadLocal. */  
        Object value;  
  
        Entry(ThreadLocal<?> k, Object v) {  
                super(k);  
                value = v;  
    }  
}  

这样的实现方式,与我们在此前介绍软引用缓存时元素的实现方式是完全相同的。java 的四种引用类型也就是说,ThreadLocalMap 实际上是一个弱引用缓存,正如我们上面这篇日志中所讲到的,如果一个对象仅仅被弱引用关联时,当下一次 GC 进行时对象就会被回收。通过弱引用实现 ThreadLocalMap 的 key,实现了没有强引用指向时的自动回收,从而实现了键的自动失效机制。

4.2. 值的获取 -- getEntry 与 getEntryAfterMiss 方法

getEntry 与 getEntryAfterMiss 方法实现了通过指定的 key 查找到对应的 value 的获取方法。

代码语言:javascript复制
private Entry getEntry(ThreadLocal<?> key) {  
        //拿到 table 索引  
        int i = key.threadLocalHashCode & (table.length - 1);  
        //得到 entry  
        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();  
                //如果 key 相等则找到了值,返回  
                if (k == key)  
                        return e;  
                //如果拿到一个 null 的 key 说明已经被回收了,需要清理  
                if (k == null)  
                        expungeStaleEntry(i);  
                else  
                        //否则的话说明需要继续寻找  
                        i = nextIndex(i, len);  
                e = tab[i];  
    }  
        return null;  
}  

4.3. 失效键值清理 -- expungeStaleEntry 方法

expungeStaleEntry 方法实现了缓存中失效 key 对应 value 的回收。

代码语言:javascript复制
private int expungeStaleEntry(int staleSlot) {  
        Entry[] tab = table;  
        int len = tab.length;  
  
        // 将 table[i] 这个引用和 value 置 null  
        tab[staleSlot].value = null;   
        tab[staleSlot] = null;  
        size--; //table 大小减一  
  
        // Rehash until we encounter null  
        Entry e;  
        int i;  
        // 从 i 位置开始遍历清理  
        for (i = nextIndex(staleSlot, len);  
                (e = tab[i]) != null;  
                i = nextIndex(i, len)) {  
            ThreadLocal<?> k = e.get();  
            //遇到还可以清理的话顺便清理掉  
            if (k == null) {  
                    e.value = null;  
                    tab[i] = null;  
                    size--;  
            } else {  
                    //遇到还没被回收的,rehash 找到新的为空的索引位置  
                    int h = k.threadLocalHashCode & (len - 1);  
                    if (h != i) {  
                            //将原位置置 null  
                            tab[i] = null;  
                            //找到新位置  
                            while (tab[h] != null)  
                                    h = nextIndex(h, len);  
                                tab[h] = e;  
                        }  
                }  
    }  
        return i;  
}  

包括 ThreadLocalMap 的 getEntry 方法调用 expungeStaleEntry,以下六个方法会调用该方法实现对失效 key 的清理:

  • cleanSomeSlots 方法 -- 清除指定失效区域,ThreadLocalMap 的 set 方法调用最后会调用该方法,进行新增键之后区域的清除,时间复杂度低只有 log2(n),但是不能保证全部失效区域被清除
  • expungeStaleEntries 方法 -- 清除所有失效区域,ThreadLocalMap 的 set 方法在调用 cleanSomeSlots 方法没有进行任何清除的情况下会调用该方法来清除所有失效区域
  • getEntryAfterMiss 方法 -- 尝试获取已失效 key 对应的 value 会调用该方法进行清除
  • remove 方法 -- 显式调用 remove 方法清除一个 key 时会调用该方法清除对应的 value
  • replaceStaleEntry 方法 -- ThreadLocalMap 的 set 方法替换某个 key 对应的值后,原存储空间的清理

5. ThreadLocal 的使用与内存泄漏

通过上述分析我们可以看到,每个线程都维持了与一个 ThreadLocalMap 关联的强引用,而每一个 ThreadLocalMap 存储的都是 Entry 的数组。通过 Entry 的实现,我们知道,ThreadLocalMap 维持了与 ThreadLocal 类型的 key 的弱引用 K-V 缓存,如果 key 失效,expungeStaleEntry 会去清理对应的值。那么,使用 ThreadLocal 有可能会造成内存泄漏吗?

5.1. ThreadLocalMap 本身的内存泄漏

但是,由于 ThreadLocalMap 是与 Thread 对象通过强引用关联的,因此 ThreadLocalMap 本身会伴随线程的整个生命周期而存在,如果在不再使用 ThreadLocalMap,我们要清理其中的所有空间,而不能将 ThreadLocalMap 对象本身置为 null。严格意义上,这就造成了内存泄漏。但是毕竟 ThreadLocalMap 本身并不会占据多少空间,存储在其中的 ThreadLocal 信息如果发生内存泄漏才是最严重的,那么,这是否会发生呢。

5.2. ThreadLocalMap 中存储的信息的内存泄漏

ThreadLocalMap 通过弱引用关联了所有的 ThreadLocal,一旦我们不再需要使用该 ThreadLocal,手动赋值为 null,或通过栈空间的回收解除强引用,下一次 GC 发生时,对应的 key 就会被回收,但这并不会触发对 value 的回收,因为 value 仍然被 Entry 以强引用关联着。虽然在 get、set 方法中都会调用 expungeStaleEntry 方法实现对失效空间的清理从而避免内存泄漏的发生,但是在不再使用时手动调用 remove 方法立即回收空间仍然是最佳的用法。

6. 参考资料

JDK 1.8。

0 人点赞