解剖ThreadLocal

提供线程内的局部变量,不同线程之间不会互相干扰,这种变量在线程的生命周期内作用,减少同一个线程内多个方法或组件之间一些公共变量传递的复杂度。

1.1、常用方法

方法声明描述
ThreadLocal() 创建threadLocal对象
public void set(T value) 设置当前线程绑定的局部变量
public T get() 获取当前线程绑定的局部变量
public void remove() 移除当前线程绑定的局部变量

1.2、应用场景

  • Spring多数据源配置的切换。

  • Spring事务注解的实现。

  • 如果开发者希望将类的某个静态变量与线程状态关联,可以考虑使用ThreadLocal。ThreadLocal的设计本身就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题。

1.3、ThreadLocal与Synchronized关键字

 synchronizedThreadLocal
原理 同步机制采用以时间换空间的方式,只提供了一份变量,让不同的线程排队访问。 ThreadLocal采用以空间换时间的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而互不干扰。
侧重点 多个线程之间访问资源同步。 多线程中让每个线程之间的数据相互隔离。

1.4、内部结构

  • 早期的设计:每个ThreadLocal都创建一个ThreadLocalMap,线程作为map的key,线程的私有变量作为map的value。

    img

  • 当前的设计:在JDK8中,每个线程中维护一个ThreadLocalMap,ThreadLocal实例作为map的key,线程的私有变量作为map的value。

    img

    具体过程:

    1. 每个Thread线程内部都有一个ThreadLocalMap。

    2. map里key为ThreadLocal对象,value是变量值。

    3. Thread内部的map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程私有变量值。

    4. 对于不同的线程,每次获取value(也就是变量值),别的线程并不能获取当前线程的变量值,形成了变量的隔离,互不干扰。

对比图:

 

 

 

如今设计的好处:

  1. 每个ThreadLocalMap存储的Entry数量变少。

  2. 当线程销毁的时候,ThreadLocalMap也会随之销毁,减少内存的使用。(之前以Thread为key会导致ThreadLocalMap的生命周期很长)

1.4.1、小结
  • 早期版本中ThreadLocalMap是由ThreadLocal维护的,map中key为线程实例,value为线程的私有变量;

  • 现在的版本中ThreadLocalMap是由线程维护的,map中的key为ThreadLocal实例,value为线程的私有变量。

  • 这样的设计既减少了ThreadLocalMap存储的Entry的数量,又因为ThreadLocalMap会随着与之相关的线程销毁而销毁,而解决了原本因为过长生命周期的ThreadLocalMap占用内存的开销。

1.5、源码分析

1.5.1、ThreadLocal源码
  • initialValue()方法:

     protected T initialValue() {
      return null;
     }
  • setInitialValue()方法:

     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);
      }
      if (this instanceof TerminatingThreadLocal) {
      TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
      }
      return value;
     }
  • createMap(Thread t, T firstValue)方法:

     void createMap(Thread t, T firstValue) {
      t.threadLocals = new ThreadLocalMap(this, firstValue);
     }
  • getMap(Thread t)方法:

     ThreadLocalMap getMap(Thread t) {
      return t.threadLocals;
     }
  • set(T value)方法:

     public void set(T value) {
      Thread t = Thread.currentThread();
      ThreadLocalMap map = getMap(t);
      if (map != null) {
      map.set(this, value);
      } else {
      createMap(t, value);
      }
     }
  • get()方法:

     ThreadLocalMap getMap(Thread t) {
      return t.threadLocals;
     }
  • remove()方法:

     public void remove() {
      ThreadLocalMap m = getMap(Thread.currentThread());
      if (m != null) {
      m.remove(this);
      }
     }
1.5.2、ThreadLocalMap源码

ThreadLocalMap是ThreadLocal的静态内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现的。

image-20220217110921157

1.6、弱引用和内存泄漏

1.6.1、术语
  • 内存溢出(OOM):是指程序在申请内存时,没有足够的内存空间供其使用;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

  • 内存泄漏:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,最终导致内存溢出。

  • 强引用:使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。

  • 弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。

1.6.2、分析

preview

ThreadLocal自身并不储存值,而是作为一个key来让线程从ThreadLocal获取value(就是从对应的key中取出value)。Entry是中的key是弱引用,所以jvm在垃圾回收时如果外部没有强引用来引用它,ThreadLocal必然会被回收(key被回收)。但是,作为ThreadLocalMap的key,ThreadLocal被回收后,ThreadLocalMap就会存在value不为null的Entry。若当前线程一直不结束,可能是作为线程池中的一员,线程结束后不被销毁,就可能引发内存泄漏。因此,key弱引用并不是导致内存泄漏的原因,而是因为ThreadLocalMap的生命周期需要当前线程销毁才结束,并且没有手动删除对应value。

Entry中的key设为弱引用的原因:设置为弱引用的key能预防大多数内存泄漏的情况。如果key 使用强引用,引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。如果key为弱引用,引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被GC回收。value在下一次ThreadLocalMap调用set、get、remove的时候就会被清除。

使用ThreadLocal发生内存泄漏的前提是:

  1. 没有手动删除Entry。

  2. 线程依然保持在的情况。

根本原因就是ThreadLocalMap和Thread一样长,Thread依然存在且没有手动删除Entry中对应的key就会导致内存泄漏。

1.6.3、避免方式

避免ThreadLocal造成内存泄漏就需要在使用完之后调用remove()方法删除对应的Entry。

posted @ 2021-08-05 16:16  是老胡啊  阅读(52)  评论(0)    收藏  举报