多线程问题的源头之一就是对共享变量的访问与修改,如果避免了共享这个条件也就可以消除并发问题。避免共享的方式有很多,例如方法中的局部变量:方法中创建的变量在方法之外无法访问,这就是线程封闭

除了使用局部变量外,Java 提供了线程本地存储(ThreadLocal)也可以做到避免变量的共享,这是线程自己的存储。下面介绍用法。

ThreadLocal 的使用方法

下面这个静态类 ThreadId 会为每个线程分配一个唯一的线程ID,如果同一个线程前后两次调用 ThreadIdget() 方法,返回的ID 相同。如果是不同线程分别调用 get 方法,则返回值不同。这是如何实现的呢?

 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();
        }
    }

看完上面的例子,再看一个例子,Java 中的 SimpleDateFormat 类是非线程安全的类,如果需要在并发场景下使用这个类,一种解决方案就是使用 ThreadLocal

下面的示例代码就是具体实现,这段代码与上面的代码高度相似,核心都是使用 ThreadLocal保证每个线程有一份自己的变量,就像方法的局部变量一样,而不是多个线程共享同一个对象。

    static class SafeDateFormat {
        // 定义ThreadLocal变量
        static final ThreadLocal<DateFormat> tl = ThreadLocal
                .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

        static DateFormat get() {
            return tl.get();
        }
        // 不同线程执行下面代码返回的 DateFormat 对象不同
        DateFormat dateFormat = SafeDateFormat.get();
    }

ThredLocal 的工作原理

首先思考如果让你实现一个类似 ThreadLocal 功能的类 —— 每个线程持有自己对应的变量,该如何实现?最直接的方法是创建一个 MapKey 是线程,V 是线程对应的变量ThreadLocal 中持有这样一个 Map 就可以实现对应的功能,示意图如下:

示例代码:

    // 自己实现一个ThreadLocal类
    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);
        }
    }

Java 中并没有让 ThreadLocal 持有这个 Map ,而是将其定义在了 Thread 类中:


/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

/*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

下面是 ThreadThreadLocal 精简后的代码,帮助理解:

class Thread {
  // 内部持有 ThreadLocalMap
  ThreadLocal.ThreadLocalMap threadLocals;
}

class ThreadLocal<T> {
  public T get() {
    // 首先获取线程持有的 ThreadLocalMap 变量
    ThreadLocalMap map = Thread.currentThread().threadLocals;
    // 在Map中查找变量
    Entry e = map.getEntry(this);
    return e.value;
  }
  static class ThreadLocalMap {
    // 底层数据结构使用数组而不是Map来保存数据
    Entry[] table;
    // 根据ThreadLocal查找Entry
    Entry getEntry(ThreadLocal key) {
      // 省略具体查找逻辑
    }
    
    // 定义 Entry 类
    static class Entry extends WeakReference<ThreadLocal> {
      Object value;
    }
  }
}

对比我们自己设计的和Java设计的不同点:

  • 我们将 Map 设计在了 ThreadLocal 类中,JDKThreadLocalMap 设计在了 Thread 中,是 Thread 所拥有的状态。 JDK 的设计更加合理,因为 ThreadLocal 仅仅是一个代理工具类,不应该持有与线程想归案的数据。

同时,JDK 的这种实现有一个更深层次的原因:不容易产生内存泄漏。在我们的设计方案中,ThreadLocal 持有的 Map 会持有 Thread 对象的引用,这意味着只要 ThreadLocal 对象头存在,Map 中的 Thread 对象就永远不会被回收。而 ThreadLocal生命周期往往都比线程要长,所以这种设计方案很容易导致内存泄漏JDK 实现中 Thread 持有 ThreadLocalMap,并且 ThreadLocalMap 使用的是 弱引用 WeakReference,只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收,这种实现方案虽然复杂一些,但是更安全

虽然 JDK 中的实现思考的更加全面,但是还是无法百分比避免内存泄漏,如果在线程池中使用 ThreadLocal,操作不当的话就可能导致内存泄漏的发生。

ThreadLocal 与内存泄漏

线程池中使用 ThreadLocal 可能导致内存泄漏的原因在于

线程池中线程的存活时间太长,往往和整个应用程序的生命周期相同,这意味着 Therad 持有的 ThreadLocalMap 一直不会被回收,ThreadLocalMap 中的 EntryThreadLocal弱引用,所以只要 ThreadLocal 的生命周期结束是可以被回收的,但是 Entry 中的 ValueEntry 强引用,所以即使 Value 的生命周期结束了,也无法被回收,从而导致了内存泄漏

解决方法也很简单,手动释放 ThreadLocal ,利用 try-finally 方案实现:

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

InheritableThreadLocal 与继承性

通过 ThreadLocal 创建的线程变量,子线程无法继承该变量。 也就是说:在线程中通过 ThreadLocal 创建了线程变量 V,然后该线程创建了子线程,在子线程无法通过 ThreadLocal 访问****父线程的线程变量 V

如果想要实现子线程继承父线程变量,JDK 提供了 InheritableThreadLocal 支持这种特性,InheritableThreadLocalThreadLocal 的子类,用法完全相同。

但是并不建议在线程池中使用 InheritableThreadLocal,因为它不仅具有 ThreadLocal 相同的缺点——可能导致内存泄漏更重要的是线程池中线程的创建是动态的,很容易导致继承关系错乱,从而导致业务逻辑计算错误,这比内存泄漏更致命。

总结

线程本地存储模式的本质上是通过避免共享的方案来解决并发问题,其使用场景在于:在并发环境中使用一个线程不安全的工具类,最简单的方案就是避免共享

避免共享有两种方案:

  • 将这个工具类作为局部变量使用,实现线程级别的封闭,其缺点在于高并发场景下会频繁创建对象
  • 使用 ThreadLocal 本地存储,每个线程只需要创建一个工具类实例,不会频繁创建对象,但是可能导致内存泄漏。

使用 ThreadLocal 需要深思熟虑,谨慎使用,因为其可能导致内存泄漏的问题发生。

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

最是人间留不住,曾是惊鸿照影来。