什么是ThreadLocal?如何正确使用ThreadLocal?

ThreadLocal线程本地存储

多个线程同时读写同一个共享变量会造成并发问题,一种解决方案就是避免变量共享。我们可以使用线程封闭技术,即使用局部变量,每个线程都有各自的调用栈,局部变量就存在栈帧中,不会与其他线程共享。我们还可以使用线程本地存储ThreadLocal

如何使用 ThreadLocal

下面这段代码会为每个线程分配一个唯一的线程Id,同一个线程每次调用 get() 获得的 Id 是一样的,不同的线程调用 get() 获得的 Id 是不一样的。

static class ThreadId {
  static final AtomicLong nextId = new AtomicLong(0);
  //定义ThreadLocal变量
  static final ThreadLocal<Long> tl=
    ThreadLocal.withInitial(()->nextId.getAndIncrement());
  //此方法会为每个线程分配一个唯一的Id
  static long get(){
    return tl.get();
  }
}

ThreadLocal

ThreadLocal 中除了构造方法还有 4 个公共的方法:

  1. get():返回此线程局部变量当前副本中的值

  2. remove():移除此线程局部变量当前副本中的值

  3. set(T value):将线程局部变量当前副本中的值设置为指定值

  4. withInitial(Supplier<? extends S> supplier):返回此线程局部变量当前副本中的初始值

ThreadLocal 的工作原理

ThreadLocal 要实现的目标是:不同的线程对应不同的变量,很自然地可以想到创建一个 Map,其中 Key 是线程,Value 是线程对应的值。那么可以让 ThreadLocal 持有一个这样的 map,并提供对应的方法,就像下面这样:

class MyThreadLocal<T> {
  Map<Thread, T> locals = 
    new ConcurrentHashMap<>();
  //获取线程变量  
  T get() {
    return locals.get(
      Thread.currentThread());
  }
  //设置线程变量
  void set(T t) {
    locals.put(
      Thread.currentThread(), t);
  }
}

这样设计会产生内存泄露的问题。ThreadLocal 持有 Map 的引用,Map 持有 Thread 对象的引用。这就意味着,只要 ThreadLocal 对象存在,那么 Map 中的 Thread 对象就不会被释放。ThreadLocal 对象的生命周期往往比线程要长得多,当长生命周期的对象持有短生命周期对象的引用,就会造成内存泄露问题

在 Java 的设计中,ThreadLocal 持有的 Map 被命名为 ThreadLocalMap。ThreadLocalMap 并不是由 ThreadLocal 持有,而是由 Thread 持有。ThreadLocal 作为一个代理工具类,内部并不持有任何与线程相关的数据,所有和线程相关的数据都存储在 Thread 里面。如下面的代码所示:

public
class Thread implements Runnable {
  // Thread 内部持有 ThreadLocalMap
  ThreadLocal.ThreadLocalMap threadLocals = null;
}

public class ThreadLocal<T> {
  
	public T get() {
    // 1. 获取线程持有的 ThreadLocalMap
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    
    // 2. 在Map中查找变量
    if (map != null) {
      ThreadLocalMap.Entry e = map.getEntry(this);
      if (e != null) {
        @SuppressWarnings("unchecked")
        T result = (T)e.value;
        return result;
      }
    }
    return setInitialValue();
  }
  
  static class ThreadLocalMap{
		
    // Entry定义,是一个弱引用
    static class Entry extends WeakReference<ThreadLocal<?>> {
      /** The value associated with this ThreadLocal. */
      Object value;
      Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
      }
    }
    
    // 内部是数组而不是Map
    private Entry[] table;
    // 根据ThreadLocal查找Entry
    Entry getEntry(ThreadLocal key){
      //省略查找逻辑
      ...
    }
  }
}

ThreadLocal各引用之间的关系

理解 ThreadLocal 的原理,要结合上面这张图和代码:

  1. 当前线程线程持有 ThreadLocalMap,ThreadLocalMap 持有 Entry;
  2. Entry 的 Key 是一个 ThreadLocal 实例,并且是一个弱引用,value 是我们要存储的值;

Java 的实现中 Thread 持有 ThreadLocalMap,ThreadLocalMap 中的 Entry 对 ThreadLocal 的引用是弱引用,所以只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收。

ThreadLocal 与内存泄露

在线程池中使用 ThreadLocal 任然可能会出现内存泄露。

因为线程池中线程的存活时间太长了,往往是和应用程序同生共死的。这就意味着 Thread 持有的 ThreadLocalMap 一直不会被回收,ThreadLocalMap 中 Entry 对 ThreadLocal 的引用是弱引用,所以只要 ThreadLocal 的生命周期结束是可以被回收的。但是 Entry 对 Value 是强引用,即使 value 的生命周期结束也无法被回收,这就造成了内存泄露

在线程池中如何正确使用 ThreadLocal?

那在线程池中,我们该如何正确使用 ThreadLocal 呢?既然 JVM 无法帮我们释放对 value 的引用,那么我们就使用 try{}finally{} 手动释放资源:

ExecutorService es;
ThreadLocal tl;
es.execute(()->{
  //ThreadLocal增加变量
  tl.set(obj);
  try {
    // 省略业务逻辑代码
  }finally {
    //手动清理ThreadLocal 
    tl.remove();
  }
});

相关文章

面试再问ThreadLocal,别说你不会

并发容器之ThreadLocal

30 | 线程本地存储模式:没有共享,就没有伤害

发布了190 篇原创文章 · 获赞 17 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/shuiCSDN/article/details/104076721