多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对同一个共享变量的写入,为了保证线程安全,一般使用者在访问共享变量时需要进行额外的同步措施。ThreadLocal 是除了加锁这种同步方式之外的另一种,规避多线程访问共享变量时发生线程不安全问题的方式。

ThreadLocal 提供线程本地变量,如果创建一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。

实现原理

在 Thread 类中有两个变量 threadLocals 和 inheritableThreadLocals,二者都是 ThreadLocalMap 类型的变量,而 ThreadLocalMap 就类似于一个 HashMap,可以对其进行 set、get 调用。
image.png
默认情况下,每个线程中的这两个变量都为 null,只有当线程第一次调用 ThreadLocal 的 set 或 get 方法时才会创建他们。因此,每个线程的本地变量实际上是放在调用线程的 ThreadLocals 变量里面的。

也就是说,ThreadLocal 类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过 set 方法将 value 添加到调用线程的 threadLocals 中,当调用线程调用 get 方法时能够从它的 threadLocals 中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的 threadLocals 中,所以不使用本地变量时需要调用 remove 方法将 threadLocals 中不用的本地变量删除。

1. set

  1. public void set(T value) {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null)
  5. map.set(this, value);
  6. else
  7. createMap(t, value);
  8. }
  9. // 获取线程自己的变量threadLocals
  10. ThreadLocalMap getMap(Thread t) {
  11. return t.threadLocals;
  12. }
  13. void createMap(Thread t, T firstValue) {
  14. t.threadLocals = new ThreadLocalMap(this, firstValue);
  15. }

每个线程都维护了一个 ThreadLocal.ThradLocalMap 变量,这是一个以 ThreadLocal 对象为键、object 对象为值的存储结构。

2. get

  1. public T get() {
  2. Thread t = Thread.currentThread();
  3. // getMap的源码就是提取线程对象t的ThreadLocalMap属性:t.threadLocals
  4. ThreadLocalMap map = getMap(t);
  5. if (map != null) {
  6. ThreadLocalMap.Entry e = map.getEntry(this);
  7. if (e != null) {
  8. return (T)e.value;
  9. }
  10. }
  11. return setInitialValue();
  12. }

每个线程都有自己的 ThreadLocalMap,如果 map==null,则直接执行 setlnitialValue()。如果 map 已经创建,就表示 Thread 类的 threadLocals 属性已经初始化,如果 e==null,依然会执行到 setlnitialValue()。

  1. private T setInitialValue() {
  2. T value = initialValue();
  3. Thread t = Thread.currentThread();
  4. ThreadLocalMap map = getMap(t);
  5. if (map != null)
  6. map.set(this, value);
  7. else
  8. createMap(t, value);
  9. return value;
  10. }
  11. // 这个时候由于是初始化,所以默认添加的值为null
  12. protected T initialValue() {
  13. return null;
  14. }

3. remove

  1. public void remove() {
  2. ThreadLocalMap m = getMap(Thread.currentThread());
  3. if (m != null)
  4. // 移除当前线程中指定ThreadLocal实例的本地变量
  5. m.remove(this);
  6. }

在正常情况下,当线程退出时 Thread 会进行一些清理工作,其中就包括清理 threadLocals。但如果使用线程池的话,当前线程未必会退出。此时,如果将一些比较大的对象设置到 ThreadLocal 中,可能会使系统出现内存泄漏(set后不进行清理)。因此最好使用 remove() 方法将这个变量移除或手动将其设置为 null。

  1. private void exit() {
  2. if (group != null) {
  3. group.threadTerminated(this);
  4. group = null;
  5. }
  6. target = null;
  7. threadLocals = null;
  8. inheritableThreadLocals = null;
  9. inheritedAccessControlContext = null;
  10. blocker = null;
  11. uncaughtExceptionHandler = null;
  12. }

ThreadLocalMap

ThreadLocal 有个静态内部类叫 ThreadLocalMap,ThreadLocalMap 内部由一个 Entry 数组构成,Entry 继承自弱引用 WeakReference,没有方法,只有一个 value 成员变量,它的 key 是 ThreadLocal 对象。

  1. static class Entry extends WeakReference<ThreadLocal<?>> {
  2. Object value;
  3. Entry(ThreadLocal<?> k, Object v) {
  4. super(k);
  5. value = v;
  6. }
  7. }

从栈与堆的内存角度看看两者的关系,如下图所示。
image.png
图中的红色虚线箭头是重点,也是整个 ThreadLocal 最难以理解的地方。

为什么需要弱引用:

所有 Entry 对象都被 ThreadLocalMap 类的实例化对象 threadLocals 持有,当线程对象执行完毕时,线程对象内的实例属性均会被垃圾回收。但如果线程没有退出,JDK 也允许我们像释放普通变量一样释放 ThreadLocal。比如,有时我们为了加速垃圾回收,会特意写出类似 obj == null 的代码。同理,对于 ThreadLocal 变量,我们也可以手动将其设置为 null。

因为 Entry 是弱引用,即使线程正在执行中,只要我们把 ThreadLocal 对象的引用置为 null,Entry 的 Key 就会自动在下一次 YGC 时被垃圾回收(因为 key 指向 ThreadLocal 对象引用)。而当 ThreadLocal 使用 set() 和 get() 时,又会自动地将那些 key==null 的 value 置为 null,使 value 能够被垃圾回收,避免内存泄漏。

所以设置为弱引用,是为了当 ThreadLocal 对象被置位 null 时可以进行自动回收,但是理想很丰满,现实很骨感,因为 ThreadLocal 对象通常作为私有静态变量使用,则其生命周期不会随线程结束而结束。

使用不当导致内存泄漏的原因:

因为 threadLocals 中的 Entry 的 key 使用的是 ThreadLocal 对象的弱引用,在没有其他地方对 threadLocals 有依赖时,threadLocals 引用的 ThreadLocal 对象就会被回收掉,那么 Entry 中的与之关联的弱引用 key 就会变成 null,如果此时当前线程还在运行,则 Entry 对应的 value 不会被回收,因为存在强引用,这就发生了内存泄漏。当然这种内存泄漏分情况,如果当前线程执行完毕会被回收,那么 Value 自然也会被回收,但如果使用线程池则 Value 会一直存在,这就发生了内存泄漏。因此,我们在使用完毕时要及时调用 remove 方法以避免内存泄漏。

InheritableThreadLocal

同一个 ThreadLocal 变量在父线程中被设置值后,在子线程中是获取不到的。因为 threadLocals 中为当前调用线程对应的本地变量,所以二者自然是不能共享的。而 InheritableThreadLocal 则可以做到这个功能。

  1. public class InheritableThreadLocal<T> extends ThreadLocal<T> {
  2. protected T childValue(T parentValue) {
  3. return parentValue;
  4. }
  5. ThreadLocalMap getMap(Thread t) {
  6. return t.inheritableThreadLocals;
  7. }
  8. void createMap(Thread t, T firstValue) {
  9. t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
  10. }
  11. }

从上面代码可以看出,InheritableThreadLocal 类继承了 ThreadLocal 类,并重写了 childValuegetMapcreateMap 三个方法。其中 createMap 方法在被调用时,创建的是 inheritableThreadLocal。同理,getMap 方法在当前调用者线程调用 get 方法时返回的也是 inheritableThreadLocal。

下面看看重写的 childValue 方法在什么时候执行,怎样让子线程访问父线程的本地变量值。首先从 Thread 类的 init 方法开始说起,该方法会在新建 Thread 实例时被执行:

  1. private void init(ThreadGroup g, Runnable target, String name,
  2. long stackSize, AccessControlContext acc,
  3. boolean inheritThreadLocals) {
  4. if (name == null) {
  5. throw new NullPointerException("name cannot be null");
  6. }
  7. this.name = name;
  8. // 获取当前线程,即新建该线程的主线程
  9. Thread parent = currentThread();
  10. ......
  11. // 如果父线程的inheritableThreadLocal不为null
  12. if (inheritThreadLocals && parent.inheritableThreadLocals != null)
  13. // 设置子线程中的inheritableThreadLocals为父线程的inheritableThreadLocals
  14. this.inheritableThreadLocals =
  15. ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  16. this.stackSize = stackSize;
  17. tid = nextThreadID();
  18. }
  19. // 相当于复制一份
  20. static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
  21. return new ThreadLocalMap(parentMap);
  22. }

可以看到,子类获取到的父类变量实际上是复制了一份。