【了不起的ThreadLocal】一、源码分析

一、前言

在JDK中,有些不起眼的类,往往蕴含着巨大的能量,ThreadLocal就是这样一个类,JDK1.2该类就诞生了,可算做JDK的一个元老了。从本篇开始,楼主打算分三篇讲解下对ThreadLocal的认知,预计会从源码分析到开源项目ThreadLocal的应用,以及ThreadLocal最有价值的分布式链路跟踪来逐步展示ThreadLocal的魅力!以下为文章标题,先立好Flag后填坑。


二、ThreadLocal数据模型

分析ThreadLocal,绕不开ThreadThreadLocalMapThreadLocalMap.Entry这个铁三角,理清这三者层次关系,是突破ThreadLocal的关键。一图胜千言,上图!

  • 每一个Thread对象都持有一个threadLocals属性,其类型为 ThreadLocalMap,它是ThreadLocal的一个静态内部类;
  • ThreadLocalMap内部维护一个ThreadLocaMap.Entry[] 数组,其中Entry的key为ThreadLocal 类型,value是客户端设置的对象;
  • Entry的key引用一个ThreadLocal 对象,这个引用比较特殊,采取的是弱引用 WeakReference

弱引用(WeakReference): 弱引用关联的对象,当GC发生时,无论内存够不够,弱引用指向的对象都会被回收。

在这里插入图片描述

Q:为什么Thread中要持有ThreadLocalMap这样一个map结构?

A:这样设计是为了能让线程有能力存储多个ThreadLocal对象,如下应用场景所示:

private static ThreadLocal<Integer> threadLocal_int = new ThreadLocal<>();

private static ThreadLocal<String> threadLocal_string = new ThreadLocal<>();

// threadLocal_int 和 threadLocal_int 具有不同的 threadLocalHashCode,故123和abc存放在同一个线程的ThreadLocalMap的2个Entry中
public void test() {
    threadLocal_int.set(123);
    threadLocal_string.set("abc");
}

三、内存泄漏

判断自己有没有掌握ThreadLocal,2个问题就可以检验出来:

  • 为什么Entry对ThreadLocal的引用设计成弱引用?
  • ThreadLocal到底存不存在内存泄漏的问题?

问题先不回答,带着这2个疑问我们进行分析,从分析中推导出答案,这样印象才能更深刻!

3.1 强引用存在内存泄漏?

换个角度思考,如果是强引用,会有什么问题?对于ThreadLocal,持有其引用的来源有两个:A.客户端持有的ThreadLocal_ref ,这个引用客户端是能操作赋值的;B.线程内部持有的ThreadLocalMap.Entry的key过来的引用,这个引用客户端完全不感知;假设客户端主动释放对ThreadLocal的引用(如:通过赋值ThreadLoca_ref= null; ),但由于还存在B来源的强引用,那ThreadLocal这个对象就一直无法被GC回收,故ThreadLocal对象存在内存泄漏风险。

扫描二维码关注公众号,回复: 10956827 查看本文章

结论:强引用存在内存泄漏风险

3.2 弱引用不存在内存泄漏?

上面分析了,强引用确实存在内存泄漏的可能性,那弱引用是不是就不存在内存泄漏风险? 来一副图接着分析: 当客户端主动释放对ThreadLocal对象的强引用(通过赋值ThreadLoca_ref = null;),GC发生时,由于Entry.key对ThreadLocal是弱引用,故ThreadLocal对象会被回收掉; 但是,Entry的value还是保持着对Object的强引用,由于Entry.key已经是null了,客户端已经没有任何方式能定位到这个Entry,故Entry的value对象存在内存泄漏风险。在这里插入图片描述

结论:弱引用也存在内存泄漏风险

PS:
1、在实际应用ThreadLocal时,几乎不会人为断开对ThreadLocal的引用。JDK给出的建议也是使用 static 修饰 ThreadLocal,这样就会一直保持着ThreadLocal的一个强引用;
2、ThreadLocalMap的get、set、remove方法都有考虑到Entry.key=null的情况。这三个操作执行时会顺带清除Entry.key=null的Entry.value (源代码里面有体现),这样大大降低了内存泄漏的发生率;
3、无论强、弱引用都存在内存泄漏的风险,为什么设计上还选择弱引用呢?原因很简单,弱引用相对强引用出现内存泄漏的概率更低一点,毕竟还能回收ThreadLocal腾点内存空间出来!

3.3 如何彻底避免内存泄漏?

之前的分析,都是分析客户端断开对ThreadLocal的引用。如果是客户端断开对Thread对象的强引用Thread_ref(通过赋值Thread_ref=null;),显然GC发生时,Thread对象由于没有被引用肯定会被干掉,ThreadLocalMap本身又是Thread对象的属性,二者同生共死。同理,顺带着ThreadLocalMap、ThreadLocal.Entry及ThreadLocal都会被GC干掉,整个堆内存干净了。但是,Thread算是一类比较稀缺的资源,实际应用往往采用线程池的来循环利用线程,故让客户端断开Thread对象的强引用来避免内存泄漏理论上可行但不符合实践。

再回顾下上面两种内存泄漏场景:强引用ThreadLocal时,内存泄漏的原因在Entry.key持有ThreadLocal的强引用,导致ThreadLocal无法被GC回收;在弱引用ThreadLocal时,内存泄漏发生在Entry.key=null的情况下,Entry的value没有途径可以被释放;显然这两种情况问题都出在Entry这个对象上,直接干掉Entry不就没内存泄漏了吗?是的,JDK的设计大师们自然想到这点,因此提供了ThreadLocal.remove()方法来干这事。

综上:避免内存泄漏的最佳实践就是当用完ThreadLocal后,主动触发ThreadLocal.remove()来清除整个Entry对象,采用如下样板代码。

ThreadLocal.set(value);
try {
   // 这里是业务代码
} finally {
    ThreadLocal.remove();
}

四、源码分析

4.1 ThreadLocal源码

对照着第一幅图,再看ThreadLocal应该不费力了。ThreadLocal常用方法有如下4个:

  • initialValue()
// 交给给客户端进行覆写自定义初始值的生成
protected T initialValue() {
    return null;
}
  • set(T value)
public void set(T value) {
    // 获得当前线程
    Thread t = Thread.currentThread();
    // 获得当前线程持有的 ThreadLocal.ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 调用ThreadLocalMap 的set方法;这里的this就是当前触发set方法的ThreadLocal对象本身
        map.set(this, value);
    else
        // 直接 new ThreadLocalMap(this, value) 并赋值给 t.threadLocals
        createMap(t, value);
}


/**
 * Get the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param  t the current thread
 * @return the map
 */
ThreadLocalMap getMap(Thread t) {
    // threadLocals 就是线程持有的 ThreadLocal.ThreadLocalMap 类型变量
    return t.threadLocals;
}
  • T get()
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 调用ThreadLocalMap 的getEntry方法
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 触发初始化initialValue方法,顺便把set方法的逻辑走一遍,最后返回初始值
    return setInitialValue();
}

/**
 * Variant of set() to establish initialValue. Used instead
 * of set() in case user has overridden the set() method.
 *
 * @return the initial value
 */
private T setInitialValue() {
    // 初始值
    T value = initialValue();
    // 下面的内容跟set方法完全一样;这才体现出 setInitialValue 这个方法名的含义!
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    // 返回初始值
    return value;
}
  • remove()
 public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         // 调用 ThreadLocalMap的remove方法
         m.remove(this);
 }

单看ThreadLocal上面的四个方法,其实还是比较清晰的。可以把ThreadLocal看做一个门面类,没有过多的逻辑,真正比较重的逻辑都委托给 ThreadLocalMap来做了。

4.2 ThreadLocalMap源码

ThreadLocalMap才是真正干活的,对应 ThreadLocal的四个方法,也提供了相应的几个方法:set() -> ThreadLocalMap.set()、get() -> getEntry(ThreadLocal<?> key)、remove() -> remove(ThreadLocal<?> key),源码如下:

  • ThreadLocalMap.set(ThreadLocal<?> key , Object value)
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

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

    // 如果ThreadLocal对应的key找得到,则进行赋值
    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) {
            // Entry值为null, 则进行清理
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 如果ThreadLocal对应的key找不到,则新建Entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 顺带做点脏数据清理工作,内部触发 expungeStaleEntry
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

// 清除key=null的Entry,显示将Entry.value=null
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) {
            // 清除key=null的Entry的value
            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;
}
  • Entry getEntry(ThreadLocal<?> key)
/**
 * Get the entry associated with key.  This method
 * itself handles only the fast path: a direct hit of existing
 * key. It otherwise relays to getEntryAfterMiss.  This is
 * designed to maximize performance for direct hits, in part
 * by making this method readily inlinable.
 *
 * @param  key the thread local object
 * @return the entry associated with key, or null if no such
 */
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
        // 什么情况下会进这里来?发生了GC,由于是弱引用,Entry的key指向的ThreadLocal对象已经被GC回收了,但Entry的value还没被清理
        return getEntryAfterMiss(key, i, e);
}

/**
 * Version of getEntry method for use when key is not found in
 * its direct hash slot.
 *
 * @param  key the thread local object
 * @param  i the table index for key's hash code
 * @param  e the entry at table[i]
 * @return the entry associated with key, or null if no such
 */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        // 拿到key指向的ThreadLocal对象
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            // key指向的ThreadLocal对象为null,说明ThreadLocal对象被垃圾回收了,故需要清理掉Entry的value
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
  • remove(ThreadLocal<?> key)
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;
        }
    }
}

4.3 小结

最后,再上一幅图,总结下ThreadLocal的set、get、remove三个方法串起来涉及到的对象和调用链路。在这里插入图片描述

结论:

  • ThreadLocal的强弱引用都存在内存泄露的风险,但这风险其实都不大。因为ThreadLocal的set、get、remove方法都额外做了清除脏数据的优化工作。
  • ThreadLocal的最佳实践是在用完ThreadLocal后主动执行remove方法,就能彻底杜绝内存泄漏!

全文终~

发布了31 篇原创文章 · 获赞 32 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/caiguoxiong0101/article/details/105355115