原创

JAVA 多线程之ThreadLocal详解

前言

通过该文,我们对ThreadLocal 、InheritableThreadLocal、TransmittableThreadLocal 的原理和源码进行深入分析,并举例讲解。

ThreadLocal 、InheritableThreadLocal是JDK自带的,解决了单线程环境和在单线程中又创建线程(父子线程)中线程隔离的问题。
TransmittableThreadLocal 是阿里巴巴的组件,主要是解决线程池中线程复用的场景。

更多 ThreadLocal 源码细节,见 《源码 ThreadLocal.java

ThreadLocal 方法

public T get(): 获取ThreadLocal中当前线程对应的线程局部变量

void set(T value):设置当前线程的线程局部变量的值

void remove():将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

protected Object initialValue(): 返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

ThreadLocal (线程本地存储) 的原理

ThreadLocal 相当于一个Map集合, 这个Map 的Key是固定的,都是当前线程。
它能够对线程隔离,让每个线程都能拥有属于自己的变量空间,线程之间互相不影响。
之所以能起到线程隔离的作用, 是因为Key就是当前的线程,所以每个线程的值都是隔离的。
因此,它存在的价值,就是: 为了线程隔离,让每个线程都能拥有属于自己的变量空间,线程之间互相不影响。

源码

ThreadLocal.java

public T get() {
    // 使用当前线程从线程中获取属性
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
      ThreadLocalMap.Entry e = map.getEntry(this);
      if (e != null) {
        @SuppressWarnings("unchecked")
        T result = (T) e.value;
        return result;
      }
    }
    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;
  }

  public void set(T value) {
    // 获取当前执行的线程, 使用当前线程作为KEY保存
    Thread t = Thread.currentThread();

    // 从 当前线程Thread 中拿到一个Map,然后把value放到这个线程的map中
    // 因为每个线程都有一个自己的Map,也就是 threadLocals,从而起到了线程隔离的作用
    ThreadLocalMap map = getMap(t);

    // 如果map不为空,说明当前线程已经构造过ThreadLocalMap,直接将值存储到map中
    if (map != null) {
      map.set(this, value);
    } else {
      // 如果map为空,说明是第一次使用,调用 createMap构造, 创建(以当前Thread变量为键值,以ThreadLocal要保存的副本变量为value)
      createMap(t, value);
    }
  }

这里的核心点在getMap中, 当前线程Thread 中拿到一个Map,然后把value放到这个线程的map中。 因为每个线程都有一个自己的Map,也就是threadLocals,从而起到了线程隔离的作用

set 方法

set方法是设置一个线程的局部变量的值,相当于当前线程通过set设置的局部变量的值,只对当前线程可见。

ThreadLocal过程分析

在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,threadLocals就是用来存储实际的变量副本的,键值为当前Thread变量,value为变量副本(即T类型的变量)。

Thread.java

  /* 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;

初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前Thread变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

以 ThreadLocal 的 set() 为例,调用 createMap 方法。
ThreadLocal 源码如下:

public void set(T value) {
    // 使用当前线程作为KEY保存
    Thread t = Thread.currentThread();

    // 从 当前线程Thread 中拿到一个Map,然后把value放到这个线程的map中
    // 因为每个线程都有一个自己的Map,也就是 threadLocals,从而起到了线程隔离的作用
    ThreadLocalMap map = getMap(t);
    if (map != null) {
      map.set(this, value);
    } else {
      // 如果从当前线程中没有取到 threadLocals, 则创建(以当前Thread变量为键值,以ThreadLocal要保存的副本变量为value)
      createMap(t, value);
    }
  }

  void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
  }

ThreadLocalMap是什么?

Map是一个集合,集合用来存储数据,在ThreadLocal中,就是用来存储线程的局部变量的。 ThreadLocalMap这个类很关键。

ThreadLocalMap getMap(Thread t) {
    //  t.threadLocals实际上就是访问Thread类中的ThreadLocalMap这个成员变量
    return t.threadLocals;
  }

从上面的代码发现每一个线程都有自己单独的ThreadLocalMap实例,而对应这个线程的所有本地变量都会保存到这个map内.

public class Thread implements Runnable {
 ...  ...

  /* 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;
}

ThreadLocalMap是在哪里构造?

在 set方法中,有一行代码 createmap(t,value);,这个方法就是用来构造ThreadLocalMap.

public class ThreadLocal<T>{
      /**
       * Create the map associated with a ThreadLocal. Overridden in
       * InheritableThreadLocal.
       *
       * @param t the current thread
       * @param firstValue value for the initial entry of the map
       */
      void createMap(Thread t, T firstValue) {
            // key其实是this,this表示当前对象的引用。 在此处this 指的是 ThreadLocal<Integer> local
            // 那么多个线程对应同一个ThreadLocal实例,怎么对每一个ThreadLocal对象做区分呢?该问题见本文后半部分。
            t.threadLocals = new ThreadLocalMap(this, firstValue);
      }
}
static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {

          /**
           * The value associated with this ThreadLocal.
           */
          Object value;

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

     ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
          // 构造一个 Entry 数组,并设置初始大小
          table = new Entry[INITIAL_CAPACITY];
          // 计算 Entry 数组下标
          int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
          // 将 firstValue 存储到指定的  table 下标中
          table[i] = new Entry(firstKey, firstValue);
          // 设置节点长度为 1
          size = 1;
          // 设置扩容的阈值
          setThreshold(INITIAL_CAPACITY);
    }
}

ThreadLocalMap是一个静态内部类,内部定义了一个Entry对象用来真正存储数据

解惑: Entry 集成了 WeakReference

WeakReference 表示弱引用,在Java中有四种引用类型,强引用、弱引用、软引用、虚引用。

使用弱引用的对象,不会阻止它所指向的对象被垃圾回收器回收。

在Java语言中, 当一个对象o被创建时, 它被放在Heap里.
当GC运行的时候, 如果发现没有任何引用指向o, o就会被回收以腾出内存空间. 也就是说, 一个对象被回收, 必须满足两个条件:

  • 没有任何引用指向它;
  • GC被运行.

在如下代码中,构造了两个对象a,b,a是对象DemoA的引用,b是对象DemoB的引用,对象DemoB同时还依赖对象DemoA,那么这个时候我们认为从对象DemoB是可以到达对象DemoA的。这种称为强可达(strongly reachable)。

DemoA a = new DemoA();
DemoB b = new DemoB(a);

如果我们增加一行代码来将a对象的引用设置为null,当一个对象不再被其他对象引用的时候,是会被GC回收的,但是对于这个场景来说,即时是a=null,也不可能被回收,因为DemoB依赖DemoA,这个时候是可能造成内存泄漏的.(因为 a 一直不会被回收)

DemoA a = new DemoA();
DemoB b = new DemoB(a);

a = null;

有两个方法可以避免这样的问题:
方法一: 全部置为 null

DemoA a = new DemoA();
DemoB b = new DemoB(a);

a = null;
b = null;

方法二: 弱引用

DemoA a = new DemoA();
WeakReference b = new WeakReference(a);

a = null;

对于第二种方式来说,DemoA只是被弱引用依赖,假设垃圾收集器在某个时间点决定一个对象是弱可达的(weakly reachable)(也就是说当前指向它的全都是弱引用),这时垃圾收集器会清除所有指向该对象的弱引用,然后把这个弱可达对象标记为可终结(finalizable)的,这样它随后就会被回收。

如果 Entry 没有使用 WeakReference 弱引用,意味着ThreadLocal的生命周期和线程是强绑定,只要线程没有销毁,那么ThreadLocal一直无法回收。而使用弱引用以后,当ThreadLocal被回收时,由于Entry的key是弱引用,不会影响ThreadLocal的回收防止内存泄漏,同时,在后续的源码分析中会看到,ThreadLocalMap本身的垃圾清理会用到这一个好处,方便对无效的Entry进行回收.

解惑:ThreadLocalMap以this作为key,怎么对每一个ThreadLocal对象做区分呢?

在构造ThreadLocalMap时,使用this作为key来存储,那么对于同一个ThreadLocal对象,如果同一个Thread中存储了多个值,是如何来区分存储的呢?

答案就在 firstKey.threadLocalHashCode&(INITIAL_CAPACITY-1)!

static class ThreadLocalMap {
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
}

threadLocalHashCode 相当于一个ThreadLocal的ID,实现的逻辑为一个散列算法:

public class ThreadLocal<T> {
        private final int threadLocalHashCode = nextHashCode();

        /**
         * The next hash code to be given out. Updated atomically. Starts at
         * zero.
         */
        private static AtomicInteger nextHashCode = new AtomicInteger();

        /**
         * The difference between successively generated hash codes - turns
         * implicit sequential thread-local IDs into near-optimally spread
         * multiplicative hash values for power-of-two-sized tables.
         */
        private static final int HASH_INCREMENT = 0x61c88647;

        private static int nextHashCode() {
            return nextHashCode.getAndAdd(HASH_INCREMENT);
        }
}

简单理解:
对于同一个ThreadLocal下的多个线程来说,当任意线程调用set方法存入一个数据到Entry中的时候,其实会根据 threadLocalHashCode生成一个唯一的id标识对应这个数据,存储在Entry数据下标中。

  • threadLocalHashCode是通过 nextHashCode.getAndAdd(HASH_INCREMENT)来实现的;

  • i*HASH_INCREMENT+HASH_INCREMENT, 每次新增一个元素(ThreadLocal)到Entry[],都会自增0x61c88647,目的为了让哈希码能均匀的分布在2的N次方的数组里。此处 i 指 : 第i次。

  • table[i] = hashCode & (length-1)

魔数0x61c88647

从上面的分析可以看出,它是在上一个被构造出的ThreadLocal的threadLocalHashCode的基础上加上一个魔数0x61c88647。

我们先来看看这个魔数的散列算法的运算结果:

public class x61c88647 {
    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;


    public static void main(String[] args) {
        magicHash(16);
        magicHash(32);
    }

    private static void magicHash(int size) {
        int hashCode = 0;

        for (int i=0; i < size; i++){
            hashCode = i * HASH_INCREMENT + HASH_INCREMENT;

            System.out.print((hashCode & (size -1)) + "  ");
        }

        System.out.println();
    }
}

输出结果

7  14  5  12  3  10  1  8  15  6  13  4  11  2  9  0  
7  14  21  28  3  10  17  24  31  6  13  20  27  2  9  16  23  30  5  12  19  26  1  8  15  22  29  4  11  18  25  0

根据运行结果,这个算法在长度为2的N次方的数组上,确实可以完美散列,没有任何冲突

魔数0x61c88647的选取和斐波那契散列有关,0x61c88647对应的十进制为1640531527。而斐波那契散列的乘数可以用(long)((1L<<31)(Math.sqrt(5)-1)); 如果把这个值给转为带符号的int,则会得到-1640531527。也就是说(long)((1L<<31)(Math.sqrt(5)-1)); 得到的结果就是1640531527,也就是魔数0x61c88647

因此, 我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。

ThreadLocal 使用场景

  • 本地化隔离保存线程特有的变量信息。
  • 数据库连接
  • Spring的事务管理,用ThreadLocal存储Connection,从而各个DAO可以获取同一Connection,可以进行事务回滚,提交等操作。
  • Session管理
  • 框架中非业务数据上下文传递(保存线程上下文信息,在任意需要的地方可以获取!!!)
  • 系统间系统变量传递(如 Dubbo)
  • 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失!!!

ThreadLocal 单线程隔离

所谓单线程隔离就是设置 ThreadLocal<Object> 的作用域。
单线程隔离,其实是为了和父子线程区分开来。

public class SingleThreadLocalTest {

    public static void main(String[] args) {
        SingleThreadLocalTest test = new SingleThreadLocalTest();
        test.thread1();
        test.thread2();
    }

    private void thread1() {
        ThreadLocal<Object> localObject = new ThreadLocal();
        localObject.set("++++++++++++++++++++++++");
        System.err.println(Thread.currentThread().getName() + "\t" + localObject.get());
    }

    private void thread2() {
        ThreadLocal<Object> localObject = new ThreadLocal();
        localObject.set("-------------------------");
        System.err.println(Thread.currentThread().getName() + "\t" + localObject.get());
    }
}

ThreadLocal 父子线程隔离与上下文传递

public class ThreadLocalTest {

    private static ThreadLocal<Object> localObject = new ThreadLocal();

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            final int key = i;
            localObject.set("B______" + key);
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    localObject.set("A______" + key);
                    System.err.println(Thread.currentThread().getName() + "\t" + localObject.get().toString());
                }
            });
            t.start();
            System.err.println(Thread.currentThread().getName() + "\t" + localObject.get());
        }
        System.err.println(Thread.currentThread().getName() + "\t" + localObject.get());
    }
}

输出

main    B______0
Thread-0    A______0
main    B______1
main    B______2
Thread-1    A______1
Thread-2    A______2
main    B______3
Thread-3    A______3
main    B______4
main    B______4
Thread-4    A______4

InheritableThreadLocal 父子线程变量继承与上下文传递

在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; (可继承)

inheritableThreadLocals 之所以可继承,是因为在Thread.init()方法中做了实现,在线程初始化时完成线程的继承传递,子线程继承父子线程属性后,直接赋值到子线程的threadLocals中。

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {

        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

Demo

public class InheritableThreadLocalsTest {

    private static ThreadLocal<Object> localObject = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            final int key = i;
            localObject.set("B______" + key);
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.err.println(Thread.currentThread().getName() + "\t" + localObject.get().toString());
                }
            });
            t.start();
            System.err.println(Thread.currentThread().getName() + "\t" + localObject.get());
        }
        System.err.println(Thread.currentThread().getName() + "\t" + localObject.get());
    }
}

输出

main    B______0
Thread-0    B______0
main    B______1
Thread-1    B______1
main    B______2
Thread-2    B______2
main    B______3
Thread-3    B______3
main    B______4
main    B______4
Thread-4    B______4

TransmittableThreadLocal 多线程下的线程复用隔离与上下文传递

在线程池中核心线程用完,并不会直接被回收,而是返回到线程池中,既然是重新利用, 那么久不会重新创建线程,不会创建线程,父子之间就不会传递(即不重新创建子线程,不能获取到最新父线程属性值)。

这种情况下不论是 ThreadLocal 还是 InheritableThreadLocal 都无法得到我们期望的结果。
阿里开源小工具类 TransmittableThreadLocal 就解决了该问题。

TransmittableThreadLocal 原理

在每次线程执行时候重新给ThreadLocal赋值

具体细节 见下文 《TransmittableThreadLocal 源码解析》

~ end

正文到此结束
广告是为了更好的提供数据服务
本文目录