ThreadLocal解析

整体结构

ThreadLocal解析_第1张图片

结构介绍:由上图可知,一条线程 Thread 包含一个 ThreadLocalMap,这个Map里面包含许多这条线程存储的局部变量值,而获取这些线程局部变量的 key 就是众多的自定义 ThreadLocal 对象的弱引用。简单概括,ThreadLocalMap 是存储在线程 Thread 里面的一个成员属性,ThreadLocal 中的内部类 ThreadLocalMap 则拥有操作这个 ThreadLocalMap 的 API ,由下面的代码可以看出来 。

 

说明:该源码来自于 jdk_1.8.0_162 版本。

 

Thread 中相关的代码结构

public class Thread implements Runnable {

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

 

ThreadLocal 代码结构

public class ThreadLocal {

    private final int threadLocalHashCode = nextHashCode();

    private static AtomicInteger nextHashCode = new AtomicInteger();

    //这个魔数的选取与 斐波那契散列 以及 黄金分割数 有关
    //当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀
    private static final int HASH_INCREMENT = 0x61c88647;
    
    private static int nextHashCode() {...}
    
    protected T initialValue() {...}
    
    public static  ThreadLocal withInitial(Supplier supplier) {...}
    
    public ThreadLocal() {}
    
    public T get() {...}
    
    private T setInitialValue() {...}
    
    public void set(T value) {...}
    
    public void remove() {...}
    
    ThreadLocalMap getMap(Thread t) {...}
    
    void createMap(Thread t, T firstValue) {...}
    
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {...}
    
    T childValue(T parentValue) {...}
    
    static final class SuppliedThreadLocal extends ThreadLocal {...}
    
    static class ThreadLocalMap {...}
    
}

 

ThreadLocal 与 Thread 关联的体现

我们先来看看最常用的 get 和 set 方法:

// get方法
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

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

关键在于这几行:

//获取当前线程的ThradLocalMap
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);

//根据ThradLocal获取对应的线程局部变量
ThreadLocalMap.Entry e = map.getEntry(this);

//将当前ThradLocal作为key向ThreadLocalMap存储线程局部变量
map.set(this, value);

说明:在调用 get 或者 set 方法时,首选会去获取到当前的线程对象,然后通过这个线程对象获取到当前线程维护的 ThreadLocalMap,再把当前的 ThreadLocal 对象(也就是this)作为 key 到 ThradLocalMap 进行获取或者存储操作,另外获取到的 thread 不能自定义,并且当前的 ThreadLocal 对象,也就是this也不能自定义,这个做法也就保证了无论什么方法调用 get、set 或者 remove,都只会操作的是当前线程的 ThreadLocalMap。

 

ThreadLocalMap 代码结构

static class ThreadLocalMap {

    //Entry继承了弱引用类WeakReference,而ThreadLocal是被弱引用的对象,所以Entry的key是使用弱引用的方式
    static class Entry extends WeakReference> {
        // 往ThreadLocal里实际塞入的值
        Object value;
        //Entry的key是使用弱引用的方式
        Entry(ThreadLocal k, Object v) {
            super(k);
            value = v;
        }
    }

    //初始容量,必须为2的幂的数
    private static final int INITIAL_CAPACITY = 16;

    //Entry数组,其大小为2的幂的数
    private Entry[] table;

    //数组里entry的个数
    private int size = 0;

    //重新分配table大小的阈值,默认为0
    private int threshold;
    
    ... ...
        
    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();

            //如果已经存在与当前key相同的Entry则覆盖其value
            if (k == key) {
                e.value = value;
                return;
            }

            //如果存在key为null的Entry,无论旧的value是否为null,都将新的value设置进去,并且根据条件清理一些无用的垃圾value
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }

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

说明:这里面有一个比较有意思的地方,那就是在set方法中的 for循环 使用到的下一个元素下标的获取方式,也就是这个方法调用:

e = tab[i = nextIndex(i, len)]

// nextIndex 方法实现:
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

结论:当 i + 1 >= len 时,也就是超过 Map 中数组下标时,又回到了首个数组的位置,所以这里我们可以推导出来 ThreadLocalMap 中 Entry 数组的结构是一个环形的,后面会有详解 。

 

ThreadLocalMap 数据结构

ThreadLocal解析_第2张图片

说明:由上图可以看出,ThreadLocalMap 里面的 Entry 使用一个环形的结构,这是为什么呢,因为 ThreadLocalMap 插入元素遇到哈希冲突时,使用的是线性探查法来处理冲突 (HashMap 使用的是拉链法),这也就导致了他是个环形。到这里可能我们又会生出新的疑问,什么是线性探测法?没关系,接下来我们好好了解一下线性探测法。

 

线性探测法(停车图借鉴而来)

 

ThreadLocal解析_第3张图片

简介:如上图,直接使用数组来存储数据。可以想象成一个停车问题。若当前车位已经有车,则你就继续往前开,直到找到下一个为空的车位 。

实现步骤

(1) 得到要插入元素的 key 。

(2) 通过哈希方法计算得到元素 key 的 hashCode,通过 hashCode 使用取模或者其他方式计算出该元素的位置 i 。

(3) 定位到位置 i,与该位置的元素对比,若不冲突,则直接填入数组 。

(4) 若冲突,则使 i++,也就是往后找,直到找到第一个 data[i] 为空的情况,则填入 。若到了尾部可循环到前面,正是因为这个,所以数组形成了一个环形 。

 

 

什么是弱引用,为什么要使用弱引用?

 

强引用:通过new创建的对象,即使OOM都不会回收。

软引用:软引用可达的对象在内存不充足时才会被回收。

弱引用:无论内存是否充足,进行GC时均会对弱引用对象进行回收。

虚引用:用作记录它指向的对象已被回收,这个虚引用只是个记录作用,甚至不能通过get获取到对应的对象。

 

回到开头的那张图片:

ThreadLocal解析_第4张图片

解析:左边的栈里面持有对右边 ThreadLocal 对象的强引用,而右边堆里面的 ThreadLocal对象 和 key 之间是弱引用的关系,正常情况下,栈依旧持有 ThreadLocal 的强引用,因为 ThreadLocal 被强引用在引用着,所以不会被回收,而当调用方在使用完这个局部变量 value 后,将左边栈里面 ThreadLocal 的对象引用设置为 null,这个时候,堆里面的ThreadLocal 对象已经没有任何强引用引用着他了,只有右边的 key 对他保持着弱引用,但这个弱引用不会影响垃圾收集器对 ThreadLocal 对象的回收,当下一次垃圾收集器进行垃圾回收时就会将 ThreadLocal 对象回收掉。所以这个弱引用的好处有以下几点:

(1) 在当前线程未结束前,将 ThreadLocal 何时回收的操作权交给调用方,使用完毕后将 ThreadLocal 的引用设置为 null 即可。

(2) 当 ThreadLocal 对象被回收后,key 的指向就变为了 null,便于后面对 value 的回收(这个下面说) 。

 

value的回收

       上面说到了 ThreadLocal 被回收后 key 就指向了 null,但是此时 Value 依旧有这一条强引用:Thread对象引用 -- Thread对象 -- ThreadLocalMap -- Entry -- Value 。在当前线程没结束前,该 Value 不会被垃圾收集器回收,但是这个 Value 对应的 Key 已经变为了 null,也就是说此时的 Value 已经变成了一个不能通过 Key 访问的对象垃圾,这种问题往往在使用线程池的情境下尤为突出,甚至有引起 OOM 的风险 。对于这个不可用的 Value 对象,ThreadLocal 中的内部类 ThreadLocalMap 里面有专门的方法来对它进行回收,方法如下:

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;
    // Rehash until we encounter null
    Entry e;
    int 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 {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;
                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

解析:这个方法将目标位置上的 value 设置为 null 后,继续检查其他位置上是否还有 key 为 null 的 value,将 value 也全部设置为 null,以便GC时回收内存空间。

那么通过哪些方法可以触发这个 expungeStaleEntry 清理方法呢,通过对 ThreadLocal 源码的阅读,最终发现可以触发该方法的操作有 get、set、remove 操作。所以,特别是在使用线程池的情况下,在使用完 ThreadLocal 后,一定要手动调用 remove 方法释放内存资源,remove 方法直接将该 Entry 所占的空间全部释放掉,如果不释放有引起 OOM 的风险。

你可能感兴趣的:(ThreadLocal解析)