讲透ThreadLocal

讲透ThreadLocal

一. 简介

ThreadLocal是JDK提供的一个工具类,其作用是在多线程共享资源的情况下,使每个线程持有一份该资源的副本,每个线程的副本都是独立互不影响的。线程操作各自的副本,这样就避免了资源竞争引发的线程安全问题。

二. 使用示例

模拟Spring中的事务管理,每个事务与当前线程绑定,不同线程的事务之间相互独立互不影响。代码如下:

public class TransactionManager {
  //业务线程
  private static final class BizTask implements Runnable{
    private final ThreadLocal<Transaction> transaction;

    public BizTask(){
      //创建事务,并与当前线程绑定
      transaction = ThreadLocal.withInitial(() -> {
        long id = Thread.currentThread().getId();
        return new Transaction(id);
      });
    }

    @Override
    public void run() {
      transaction.get().begin();
      System.err.println("执行业务逻辑");
      try {
        Thread.sleep(1000L);
        transaction.get().commit();
        transaction.remove();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  //事务
  private static final class Transaction {
    private long id;
    private TransactionStatus status;

    public Transaction(long id) {
      this.id = id;
    }

    public void begin(){
      status = TransactionStatus.BEGIN;
      System.err.println("开启事务, id: " + id + ", status: " + status);
    }

    public void commit() {
      status = TransactionStatus.COMMIT;
      System.err.println("提交事务, id: " + id + ", status: " + status);
    }

    public void rollback() {
      status = TransactionStatus.ROLLBACK;
      System.err.println("回滚事务, id: " + id + ", status: " + status);
    }
  }

  //事务状态
  private enum TransactionStatus {
    BEGIN,
    COMMIT,
    ROLLBACK,

  }

  public static void main(String[] args) throws InterruptedException {
    int threadNum = 5;
    ExecutorService executor = Executors.newFixedThreadPool(threadNum);

    //执行业务逻辑,可以看到每个线程的事务相互独立,互不影响
    for (int i = 0; i < threadNum; i++) {
      executor.submit(new BizTask());
    }
    executor.shutdown();

  }

}

结果如下:

开启事务, id: 17, status: BEGIN
执行业务逻辑
开启事务, id: 14, status: BEGIN
执行业务逻辑
开启事务, id: 13, status: BEGIN
执行业务逻辑
开启事务, id: 16, status: BEGIN
执行业务逻辑
开启事务, id: 15, status: BEGIN
执行业务逻辑
提交事务, id: 14, status: COMMIT
提交事务, id: 17, status: COMMIT
提交事务, id: 13, status: COMMIT
提交事务, id: 16, status: COMMIT
提交事务, id: 15, status: COMMIT

三. ThreadLocal源码分析

ThreadLocal,线程本地变量,该变量为每个线程私有。ThreadLocal类有一个内部类,名为ThreadLocalMap,可以理解为一个简化版的HashMap。源码如下:

static class ThreadLocalMap {
  //该Map的Entry,Key为ThreadLocal实例,Value为ThreadLocal对象所引用的值。
  //这里使用了WeakReference弱引用,当Entry为null时可以尽快被GC
  static class Entry extends WeakReference<ThreadLocal<?>> {
		//与ThreadLocal关联的对象引用,为当前Entry的value
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
      super(k);
      value = v;
    }
  }

  //初始容量16
  private static final int INITIAL_CAPACITY = 16;

  //使用数组保存了所有的线程本地变量
  private Entry[] table;
}

其中Entry为ThreadLocalMap的一个内部类,与HashMap的Entry结构类似,都是key-value对的形式。它的Key为ThreadLocal实例,Value为ThreadLocal对象所引用的对象。ThreadLocalMap使用一个Entry[]数组保存了所有的线程本地变量,因为一个线程可以维护多个ThreadLocal实例。

ThreadLocalMap内部保存了众多的ThreadLocal对象,既然说ThreadLocal是线程私有的,那么ThreadLocalMap是存放在哪里呢?

Thread类有一个成员变量——threadLocals,它就是保存了与当前Thread关联的一个ThreadLocalMap,源码如下:

//当前线程内部维护的ThreadLocalMap对象,用于保存所有ThreadLocal实例
ThreadLocal.ThreadLocalMap threadLocals = null;

可以看到,ThreadLocalMap对象保存在了Thread的内部,也即当前线程的私有内存中。通过上面的分析,我们可以了解ThreadLocal变量的大致内存结构如下:

在这里插入图片描述

ThreadLocal的主要方法为get()、set()和initialValue()。首先看set():

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    
    //获取当前线程关联的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    
    //创建一个Entry,以当前ThreadLocal对象为Key,待存储对象为Value,保存在ThreadLocalMap中
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

可以看到,set()的逻辑很简单,从当前线程中获取ThreadLocalMap,然后将该ThreadLocal的值保存在里面。

再看get()方法:

public T get() {
    //获取当前线程对象
    Thread t = Thread.currentThread();
    
    //获取当前线程关联的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    
    //从ThreadLocalMap获取以ThreadLocal为key的Entry的value
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    
    //如果当前ThreadLocalMap不存在,则调用setInitialValue()方法,获取初始值
    return setInitialValue();
}

ThreadLocal还有一个方法initialValue(),该方法提供给子类覆盖,以在创建ThreadLocal时指定初始值。

四. 应用场景

ThreadLocal最常见的使用场景为管理数据库连接Connection对象等。Spring中使用ThreadLocal来设计TransactionSynchronizationManager类,实现了事务管理与数据访问服务的解耦,同时也保证了多线程环境下connection的线程安全问题。

五. ThreadLocal的内存泄漏问题

  1. JVM引用类型

    从源码中可以看出,ThreadLocalMap中Entry的key为ThreadLocal对象,并将其声明成了一个WeakReference弱引用。在分析其设计思想之前,先简单回顾下JVM中的几种引用类型:

    1. 强应用:普通的引用类型,被强引用的对象是一定不会被GC回收的。
    2. 软引用SoftReference:软引用一般用来实现内存敏感的缓存,如果有空闲内存就可以保留缓存,当内存不足时就清理掉,这样就保证使用缓存的同时不会耗尽内存。
    3. 弱引用WeakReference:它的生命周期比软引用还要短,在GC的时候,不管内存空间是否够用,都会回收WeakReference对象。
    4. 虚引用PhantomReference(较少使用):任何时候可能被GC回收,就像没有引用一样。

    ThreadLocal之所以设计成WeakReference,目的就是当外部不再持有ThreadLocal的强引用时,尽快回收该ThreadLocalMap中对应的key。

  2. ThreadLocal的内存泄漏问题

    如前文所述,当ThreadLocal没有外部强引用来引用它时,ThreadLocal对象就会在下次JVM垃圾收集时被回收,这个时候就会出现Entry中Key已经被回收,但是Value仍然所在,即所谓的"null Key"情况。此时外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就会一直存在一条强引用链:Thread --> ThreadLocalMap–>Entry–>Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收了,进而造成内存泄漏。

  3. JDK对于ThreadLocal内存泄漏的解决方案

    JDK团队已经考虑到这样的情况,并做了一些措施来使得ThreadLocal尽量不会出现内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候,会清除掉ThreadLocalMap的所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。

    以ThreadLocal的get()方法为例:

    public T get() {
      //获取当前线程实例
      Thread t = Thread.currentThread();
      
      //获取当前线程中的ThreadLocalMap
      ThreadLocalMap map = getMap(t);
      if (map != null) {
        
        //通过ThreadLocalMap的getEntry()方法获取Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
          @SuppressWarnings("unchecked")
          T result = (T)e.value;
          return result;
        }
      }
      return setInitialValue();
    }
    

    它会调用ThreadLocalMap的getEntry()方法获取Entry实例:

    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
        //如果Entry获取不到,则调用getEntryAfterMiss()
        return getEntryAfterMiss(key, i, e);
    }
    

    如果获取不到,说明该key不存在或已经被回收了,则进入getEntryAfterMiss()方法:

    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
      Entry[] tab = table;
      int len = tab.length;
    
      while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
          return e;
        
        //如果key为null,则清除该Entry项
        if (k == null)
          expungeStaleEntry(i);
        else
          i = nextIndex(i, len);
        e = tab[i];
      }
      return null;
    }
    

    注意这里的k == null的情况,如果key为null,则执行expungeStaleEntry清除该Entry项。

    此外,JDK推荐当ThreadLocal对象不再使用时,显式调用其remove()方法,清除该线程本地变量,最终也会调用上面的ThreadLocalMap.getEntryAfterMiss()方法。

    综上,JDK也提供了相应的策略尽量避免ThreadLocal导致的内存泄漏问题,主要是在get()和remove()操作时清除已被回收的Entry项。但是该策略也并不是完美的,如果用户将ThreadLocal初始化后,再也不调用get()或remove()方法,则还是有内存泄漏的风险。

  4. 为什么要使用WeakReference?

    从前文的描述中,很可能造成一种错觉:ThreadLocal由于使用了WeakReference而导致了内存泄漏。这其实是没有真正理解ThreadLocal内存泄漏的本质。首先我们来看看为什么要使用弱引用。下面是官方文档的说法:

    To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. 
    Entry的key使用WeakReference弱引用,来处理大内存、长生命周期的线程的使用问题。
    

    我们分别考虑下不使用WeakReferences和使用WeakReferences的情况下,分别会造成什么问题:

    1. 不使用WeakReferences,而使用强引用:外部引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致整个Entry内存泄漏。
    2. 使用使用WeakReferences:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set()、get()或remove()的时候会被清除。如果没有调用,则可能造成Entry的Value的内存泄漏。

    由此可以看出,无论是否使用WeakReference,都有可能产生内存泄漏的情况,其根本原因在于ThreadLocalMap的生命周期与线程绑定。如果线程存活时间较长,且没有显式remove掉ThreadLocal对象,就有可能出现问题。而使用了WeakReference,至少可以保证无用的ThreadLocal对象被回收,不会出现整个Entry的内存泄漏,在一定程度上缓解了该问题。

  5. 总结

    ThreadLocal的内存泄漏问题,根本原因是ThreadLocalMap的生命周期与Thread绑定,如果线程执行时间较长,则ThreadLocalMap就会一直不被GC回收。如果不显式调用remove()方法移除过期的ThreadLocal,则有可能造成内存泄漏。因此建议使用ThreadLocal时线程生命周期不要过长,且ThreadLocal对象使用完后显式调用remove()方法进行移除。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章