ThreadLocal源码分析-黄金分割数的使用(下)

简介: 最近接触到的一个项目要兼容新老系统,最终采用了ThreadLocal(实际上用的是InheritableThreadLocal)用于在子线程获取父线程中共享的变量。问题是解决了,但是后来发现对ThreadLocal的理解不够深入,于是顺便把它的源码阅读理解了一遍。在谈到ThreadLocal之前先卖个关子,先谈谈黄金分割数。本文在阅读ThreadLocal源码的时候是使用JDK8(1.8.0_181)。

ThreadLocal源码分析



ThreadLocal的创建


ThreadLocal的构造函数来看,ThreadLocal实例的构造并不会做任何操作,只是为了得到一个ThreadLocal的泛型实例,后续可以把它作为ThreadLocalMap$Entry的键:


// 注意threadLocalHashCode在每个新`ThreadLocal`实例的构造同时已经确定了
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// 通过Supplier去覆盖initialValue方法
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}
// 默认公有构造函数
public ThreadLocal() {
}
复制代码


注意threadLocalHashCode在每个新ThreadLocal实例的构造同时已经确定了,这个值也是Entry哈希表的哈希槽绑定的哈希值。


TreadLocal的set方法


ThreadLocalset()方法的源码如下:


public void set(T value) {
  //设置值前总是获取当前线程实例
    Thread t = Thread.currentThread();
  //从当前线程实例中获取threadLocals属性
    ThreadLocalMap map = getMap(t);
    if (map != null)
       //threadLocals属性不为null则覆盖key为当前的ThreadLocal实例,值为value
         map.set(this, value);
    else
  //threadLocals属性为null,则创建ThreadLocalMap,第一个项的Key为当前的ThreadLocal实例,值为value
        createMap(t, value);
}
// 这里看到获取ThreadLocalMap实例时候总是从线程实例的成员变量获取
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
// 创建ThreadLocalMap实例的时候,会把新实例赋值到线程实例的threadLocals成员
void createMap(Thread t, T firstValue) {
     t.threadLocals = new ThreadLocalMap(this, firstValue);
}
复制代码


上面的过程源码很简单,设置值的时候总是先获取当前线程实例并且操作它的变量threadLocals。步骤是:


  • 获取当前运行线程的实例。
  • 通过线程实例获取线程实例成员threadLocals(ThreadLocalMap),如果为null,则创建一个新的ThreadLocalMap实例赋值到threadLocals。
  • 通过threadLocals设置值value,如果原来的哈希槽已经存在值,则进行覆盖。


微信截图_20220512205615.png

微信截图_20220512205624.png

微信截图_20220512205632.png


TreadLocal的get方法


ThreadLocalget()方法的源码如下:


public T get() {
   //获取当前线程的实例
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    //根据当前的ThreadLocal实例获取ThreadLocalMap中的Entry,使用的是ThreadLocalMap的getEntry方法
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T) e.value;
             return result;
            }
        }
    //线程实例中的threadLocals为null,则调用initialValue方法,并且创建ThreadLocalMap赋值到threadLocals
    return setInitialValue();
}
private T setInitialValue() {
    // 调用initialValue方法获取值
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // ThreadLocalMap如果未初始化则进行一次创建,已初始化则直接设置值
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
protected T initialValue() {
     return null;
}
复制代码


initialValue()方法默认返回null,如果ThreadLocal实例没有使用过set()方法直接使用get()方法,那么ThreadLocalMap中的此ThreadLocal为Key的项会把值设置为initialValue()方法的返回值。如果想改变这个逻辑可以对initialValue()方法进行覆盖。


微信截图_20220512205642.png


TreadLocal的remove方法


ThreadLocalremove()方法的源码如下:


public void remove() {
  //获取Thread实例中的ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
     //根据当前ThreadLocal作为Key对ThreadLocalMap的元素进行移除
       m.remove(this);
}
复制代码


微信截图_20220512205652.png


ThreadLocal.ThreadLocalMap的初始化



我们可以关注一下java.lang.Thread类里面的变量:


public class Thread implements Runnable {
  //传递ThreadLocal中的ThreadLocalMap变量
  ThreadLocal.ThreadLocalMap threadLocals = null;
  //传递InheritableThreadLocal中的ThreadLocalMap变量
  ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
复制代码


也就是,ThreadLocal需要存放和获取的数据实际上绑定在Thread实例的成员变量threadLocals中,并且是ThreadLocal#set()方法调用的时候才进行懒加载的,可以结合上一节的内容理解一下,这里不展开。


什么情况下ThreadLocal的使用会导致内存泄漏



其实ThreadLocal本身不存放任何的数据,而ThreadLocal中的数据实际上是存放在线程实例中,从实际来看是线程内存泄漏,底层来看是Thread对象中的成员变量threadLocals持有大量的K-V结构,并且线程一直处于活跃状态导致变量threadLocals无法释放被回收。threadLocals持有大量的K-V结构这一点的前提是要存在大量的ThreadLocal实例的定义,一般来说,一个应用不可能定义大量的ThreadLocal,所以一般的泄漏源是线程一直处于活跃状态导致变量threadLocals无法释放被回收。但是我们知道,·ThreadLocalMap·中的Entry结构的Key用到了弱引用(·WeakReference<ThreadLocal<?>>·),当没有强引用来引用ThreadLocal实例的时候,JVM的GC会回收ThreadLocalMap中的这些Key,此时,ThreadLocalMap中会出现一些Key为null,但是Value不为null的Entry项,这些Entry项如果不主动清理,就会一直驻留在ThreadLocalMap中。也就是为什么ThreadLocalget()set()remove()这些方法中都存在清理ThreadLocalMap实例key为null的代码块。总结下来,内存泄漏可能出现的地方是:


  • 1、大量地(静态)初始化ThreadLocal实例,初始化之后不再调用get()set()remove()方法。
  • 2、初始化了大量的ThreadLocal,这些ThreadLocal中存放了容量大的Value,并且使用了这些ThreadLocal实例的线程一直处于活跃的状态。


ThreadLocal中一个设计亮点是ThreadLocalMap中的Entry结构的Key用到了弱引用。试想如果使用强引用,等于ThreadLocalMap中的所有数据都是与Thread的生命周期绑定,这样很容易出现因为大量线程持续活跃导致的内存泄漏。使用了弱引用的话,JVM触发GC回收弱引用后,ThreadLocal在下一次调用get()set()remove()方法就可以删除那些ThreadLocalMap中Key为null的值,起到了惰性删除释放内存的作用。


其实ThreadLocal在设置内部类ThreadLocal.ThreadLocalMap中构建的Entry哈希表已经考虑到内存泄漏的问题,所以ThreadLocal.ThreadLocalMap$Entry类设计为弱引用,类签名为static class Entry extends WeakReference<ThreadLocal<?>>。之前一篇文章介绍过,如果弱引用关联的对象如果置为null,那么该弱引用会在下一次GC时候回收弱引用关联的对象。举个例子:


public class ThreadLocalMain {
  private static ThreadLocal<Integer> TL_1 = new ThreadLocal<>();
  public static void main(String[] args) throws Exception {
    TL_1.set(1);
    TL_1 = null;
    System.gc();
    Thread.sleep(300);
  }
}
复制代码


这种情况下,TL_1这个ThreadLocal在主动GC之后,线程绑定的ThreadLocal.ThreadLocalMap实例中的Entry哈希表中原来的TL_1所在的哈希槽Entry的引用持有值referent(继承自WeakReference)会变成null,但是Entry中的value是强引用,还存放着TL_1这个ThreadLocal未回收之前的值。这些被"孤立"的哈希槽Entry就是前面说到的要惰性删除的哈希槽。


ThreadLocal的最佳实践



其实ThreadLocal的最佳实践很简单:


  • 每次使用完ThreadLocal实例,都调用它的remove()方法,清除Entry中的数据。


调用remove()方法最佳时机是线程运行结束之前的finally代码块中调用,这样能完全避免操作不当导致的内存泄漏,这种主动清理的方式比惰性删除有效。


父子线程数据传递InheritableThreadLocal



留待下一篇文章编写,因为InheritableThreadLocal只能通过父子线(1->1)程传递变量,线程池里面的线程有可能是多个父线程共享的(也就是1个父线程提交的任务有可能由线程池中的多个子线程执行),因此有可能出现问题。阿里为了解决这个问题编写过一个框架-transmittable-thread-local,解决了父线程和线程池中线程的变量传递问题。


小结



ThreadLocal线程本地变量是线程实例传递和存储共享变量的桥梁,真正的共享变量还是存放在线程实例本身的属性中。ThreadLocal里面的基本逻辑并不复杂,但是一旦涉及到性能影响、内存回收(弱引用)和惰性删除等环节,其实它考虑到的东西还是相对全面而且有效的。


个人博客




相关文章
|
5月前
|
SQL Java 数据库连接
剑指JUC原理-15.ThreadLocal(上)
剑指JUC原理-15.ThreadLocal
76 1
|
5月前
|
监控 Java 应用服务中间件
剑指JUC原理-3.线程常用方法及状态(下)
剑指JUC原理-3.线程常用方法及状态
81 0
|
5月前
|
存储 Java 数据安全/隐私保护
剑指JUC原理-15.ThreadLocal(中)
剑指JUC原理-15.ThreadLocal
47 0
|
5月前
|
安全 算法 Java
剑指JUC原理-19.线程安全集合(上)
剑指JUC原理-19.线程安全集合
46 0
|
4月前
|
安全
ConcurrentHashMap1.7分段锁原理
ConcurrentHashMap1.7分段锁原理
33 0
|
2月前
|
Java
【Java集合类面试十二】、HashMap为什么线程不安全?
HashMap在并发环境下执行put操作可能导致循环链表的形成,进而引起死循环,因而它是线程不安全的。
|
5月前
|
安全 Java 调度
HashMap很美好,但线程不安全怎么办?ConcurrentHashMap告诉你答案!
HashMap很美好,但线程不安全怎么办?ConcurrentHashMap告诉你答案!
89 1
|
5月前
|
存储 算法 安全
9张图深入剖析ConcurrentHashMap
9张图深入剖析ConcurrentHashMap
|
5月前
|
算法 安全 Java
20.Atmoic系列Strimped64分段锁底层实现源码剖析
20.Atmoic系列Strimped64分段锁底层实现源码剖析
28 0
20.Atmoic系列Strimped64分段锁底层实现源码剖析
|
5月前
|
调度
剑指JUC原理-3.线程常用方法及状态(上)
剑指JUC原理-3.线程常用方法及状态(上)
56 0