浅析ThreadLocal

1,608 阅读7分钟

文章顶部.png author-nanbei

前言

作为一枚应届毕业菜鸟,在面试中经常被考察的题目:什么是 ThreadLocal?ThreadLocal 的底层原理?以及在实际开发项目过程中,经常用到保存用户信息的类就是 ThreadLocal。

ThreadLocal 是 Java 编程语言中的一个线程本地变量,它为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。

那么 ThreadLocal 到底解决了什么问题,又适用于什么样的场景?以及 ThreadLocal 一些细节!ThreadLocal 的最佳实践!今天,我们就从这些角度出发,浅析一下 ThreadLocal。

1. ThreadLocal 的原理

ThreadLocal 的基本原理是通过在每个线程中创建一个 ThreadLocal 对象来实现的。该对象内部维护了一个 Map 数据结构,用于存储每个线程对应的 ThreadLocal 变量值。当一个线程需要访问 ThreadLocal 变量时,它会首先获取当前线程对应的 ThreadLocal 对象,然后通过该对象来访问和修改 ThreadLocal 变量的值。由于每个线程都有自己的 ThreadLocal 对象,因此它们之间互不影响,从而实现了线程之间的隔离。

我们如果想要真正理解 ThreadLocal,就得从源码看起。

1.1 ThreadLocal 的 set()方法

ThreadLocal 的 set()方法用于设置当前线程对应的 ThreadLocal 变量的值。

具体来说,set()方法接受一个参数,即要设置的值。当调用 set()方法时,它会首先获取当前线程对应的 ThreadLocal 对象,然后将该值存储到 ThreadLocal 对象中。由于每个线程都有自己的 ThreadLocal 对象,因此它们之间互不影响,从而实现了线程之间的隔离。

public void set(T value) {
  //1、获取当前线程
  Thread t = Thread.currentThread();
  //2、获取线程中的属性 threadLocalMap ,如果 threadLocalMap 不为空,
  //则直接更新要保存的变量值,否则创建 threadLocalMap,并赋值
  ThreadLocalMap map = getMap(t);
  if (map != null)
      map.set(this, value);
  else
      // 初始化 thradLocalMap 并赋值
      createMap(t, value);
}

1.2 ThreadLocal 的 get()方法

ThreadLocal 的 get()方法用于获取当前线程对应的 ThreadLocal 变量的值。

public T get() {
  //1、获取当前线程
  Thread t = Thread.currentThread();
  //2、获取当前线程的 ThreadLocalMap
  ThreadLocalMap map = getMap(t);
  //3、如果 map 数据不为空,
  if (map != null) {
      //3.1、获取 threalLocalMap 中存储的值
      ThreadLocalMap.Entry e = map.getEntry(this);
      if (e != null) {
          @SuppressWarnings("unchecked")
          T result = (T)e.value;
          return result;
      }
  }
  //如果是数据为 null,则初始化,初始化的结果,TheralLocalMap 中存放 key 值为 threadLocal,值为 null
  return 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);
  return value;
}

1.3 ThreadLocal 的 remove()方法

ThreadLocal 的 remove()方法用于移除当前线程对应的 ThreadLocal 变量的值。

public void remove() {
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null) {
      map.remove(this);
  }
}

当调用 remove 方法时,ThreadLocal 变量将从当前线程的 ThreadLocalMap 中删除,以便垃圾回收器可以回收它所引用的对象。如果没有调用 remove 方法,则可能会导致内存泄漏问题 。

1.4 ThreadLocalMap

ThreadLocalMap 是 ThreadLocal 类中的一个内部类,用于存储每个线程的 ThreadLocal 变量。具体来说,ThreadLocalMap 是一个 HashMap 对象,其中键为 ThreadLocal 对象,值为对应的 ThreadLocal 变量值。

ThreadLocalMap 的作用是为了实现线程之间的隔离。每个线程都有自己的 ThreadLocalMap 对象,用于存储该线程的 ThreadLocal 变量。当需要访问 ThreadLocal 变量时,首先会通过 get()方法获取对应的 ThreadLocal 对象,然后通过该对象来访问和修改 ThreadLocal 变量的值。如果当前线程没有对应的 ThreadLocalMap 对象,则会先初始化该线程的 ThreadLocalMap 对象,并将该线程的 ThreadLocal 变量存储在 initialValue 字段中。

需要注意的是,由于每个线程都有自己的 ThreadLocalMap 对象,因此它们之间互不影响,从而实现了线程之间的隔离。同时,由于 ThreadLocalMap 对象是由 ThreadLocal 类内部维护的,因此不需要手动进行垃圾回收。

成员变量

  //初始容量,必须是 2 的整次幂
   private static final int INITIAL_CAPACITY = 16;
   //存放数据的 Table,长度也必须是 2 的整次幂
   private ThreadLocal.ThreadLocalMap.Entry[] table;
   //数组里面 entrys 的个数,可以用于判断 table 当前使用量是否超过阈值
   private int size = 0;
   //进行扩容的阈值,表使用量大于它的时候进行扩容。
   private int threshold; // Default to 0

存储结构-Entry

static class Entry extends WeakReference<ThreadLocal<?>> {
  /** The value associated with this ThreadLocal. */
  Object value;
  Entry(ThreadLocal<?> k, Object v) {
    super(k);
    value = v;
  }
}

在 ThreadLocalMap 中,用 Entry 保存 key-v 结构。不过 Entry 中的 key 只能是 ThreadLocal 对象,并且 key 是弱引用,只能活到下次 GC 开始,目的是将 ThreadLocal 对象的生命周期和线程生命周期进行解绑。

2. 一个简单的 ThreadLocal

public class MyThread extends Thread implements Runnable {
  private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
  public void run() {
    threadLocal.set(10); // 在当前线程中设置变量值为 10
    try {
        Integer value = threadLocal.get(); // 从当前线程中获取变量值为 10
        System.out.println("从当前线程中获取的变量值为:" + value);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        threadLocal.remove(); // 在当前线程中移除变量值为 10 的 ThreadLocal 对象
    }
  }
}

在这个示例中,MyThread 类继承了 Thread 类和 Runnable 接口,并实现了 run()方法。在 run()方法中,我们使用了 ThreadLocal 对象来存储一个整数变量,并在当前线程中设置和获取该变量的值。当创建 MyThread 对象时,会自动调用其构造函数,并将该对象作为参数传递给 Thread 类的构造函数。然后,Thread 类会启动该线程,并调用 MyThread 对象的 run()方法来执行线程任务。

除了继承 Thread 类和实现 Runnable 接口之外,还可以使用匿名内部类来创建 Thread 对象。例如:

new Thread(new Runnable() {
  @Override
  public void run() {
    threadLocal.set(10); // 在当前线程中设置变量值为 10
    try {
        Integer value = threadLocal.get(); // 从当前线程中获取变量值为 10
        System.out.println("从当前线程中获取的变量值为:" + value);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        threadLocal.remove(); // 在当前线程中移除变量值为 10 的 ThreadLocal 对象
    }
  }
}).start();

在这个示例中,我们使用了匿名内部类来创建 Thread 对象,并将其作为参数传递给 Thread 类的构造函数。然后,我们调用 Thread 对象的 start()方法来启动线程。

3. ThreadLocal 的使用场景

  1. 多线程环境下共享数据,避免数据竞争问题。例如,多个线程需要同时访问同一个计数器、队列等数据结构时,可以使用 ThreadLocal 来为每个线程提供独立的变量副本,从而避免了数据竞争问题。

  2. 用于存储线程上下文信息,如用户 ID、请求 ID 等。例如,在 Web 应用程序中,每个请求都会创建一个 ThreadLocal 对象,用于存储该请求的上下文信息,如用户 ID、请求 ID 等。

  3. 实现线程安全的日志记录功能。例如,在 Web 应用程序中,可以使用 ThreadLocal 来为每个线程提供独立的日志记录器,从而实现线程安全的日志记录功能。

  4. 在测试框架中使用 ThreadLocal 来隔离测试用例之间的数据。例如,在 JUnit 测试框架中,可以使用 ThreadLocal 来为每个测试用例提供独立的测试数据,从而避免了测试用例之间的数据干扰问题。

  5. 最常见的 ThreadLocal 使用场景为用来解决数据库连接、Session 管理等。代码如下:

    //数据库连接
    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
      public Connection initialValue() {  
        return DriverManager.getConnection(DB_URL);  
      }  
    };  
      
    public static Connection getConnection() {  
      return connectionHolder.get();  
    }  
    
    //Session 管理
    private static final ThreadLocal threadSession = new ThreadLocal();  
    
    public static Session getSession() throws InfrastructureException {  
      Session s = (Session) threadSession.get();  
      try {  
        if (s == null) {  
            s = getSessionFactory().openSession();  
            threadSession.set(s);  
        }  
      } catch (HibernateException ex) {  
        throw new InfrastructureException(ex);  
      }  
      return s;  
    }  
    

4. ThreadLocal 的一些细节

需要注意的是,由于每个线程都有自己的 ThreadLocal 对象,因此在使用 ThreadLocal 变量时,需要确保每个线程都正确地初始化了自己的 ThreadLocal 对象。否则可能会导致多个线程同时访问同一个 ThreadLocal 变量的情况发生,从而导致数据不一致的问题。

ThreadLocal 还有一个需要注意的潜在风险,就是内存泄露。

内存泄露产生的原因如下:

image-20230713154734371

如图所示存在一条引用链:Thread Ref->Thread->ThreadLocalMap->Entry->Key:Value。

经过上面的讲解我们知道 ThreadLocal 作为 Key,但是被设置成了弱引用。弱引用在 JVM 垃圾回收时是优先回收的,就是说无论内存是否足够弱引用对象都会被回收;弱引用的生命周期比较短;当发生一次 GC 的时候就会变成如下:

image-20230713154923280

ThreadLocalMap 中出现了 Key 为 null 的 Entry,此时没有办法访问这些 key 为 null 的 Entry 的 value。如果线程迟迟不结束,就会由于 value 永远无法回收而造成内存泄露。如果当前线程运行结束 Thread,ThreadLocalMap 和 Entry 之间没有了引用链,在垃圾回收的时候就会被回收。但是在开发中我们都是使用线程池的方式,线程池的复用不会主动结束,所以还是会存在内存泄露问题。

这个问题解决方法也很简单,就是在我们在使用完 ThreadLocal 方法后手动调用 remove()方法清除数据。在使用线程池的情况下,没有及时清理 ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用 ThreadLocal 就跟加锁完要解锁一样,用完就清理。

推荐阅读

图可视化探索与实践

ES亿级商品索引拆分实战

在 ARM 环境下搭建原生 Hadoop 集群

利用流量保障搜索质量的实践

Rc-form: 消失的“Ta”

招贤纳士

政采云技术团队(Zero),Base 杭州,一个富有激情和技术匠心精神的成长型团队。规模 500 人左右,在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。

如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注 文章顶部.png