多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步,如图 1-3 所示。

同步的措施一般是加锁,这就需要使用者对锁有一定的了解,这显然加重了使用者的负担。那么有没有一种方式可以做到,当创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量呢?其实 ThreadLocal 就可以做这件事情,虽然 ThreadLocal 并不是为了解决这个问题而出现的。

ThreadLocal - 图1

图 1-3

ThreadLocal 是 JDK 包提供的,它提供了线程本地变量,也就是如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个 ThreadLocal 变量后,每个线程都会复制一个变量到自己的本地内存,如图 1-4 所示。

ThreadLocal - 图2

图 1-4

ThreadLocal 使用示例

本节介绍如何使用 ThreadLocal。本例开启了两个线程,在每个线程内部都设置了本地变量的值,然后调用 print 函数打印当前本地变量的值。如果打印后调用了本地变量的 remove 方法,则会删除本地内存中的该变量,代码如下。

  1. public class ThreadLocalTest {
  2. //(1)print 函数
  3. static void printString str){
  4. //1.1 打印当前线程本地内存中 localVariable 变量的值
  5. System.out.println(str + 「:」 +localVariable.get());
  6. //1.2 清除当前线程本地内存中的 localVariable 变量
  7. //localVariable.remove();
  8. }
  9. //(2) 创建 ThreadLocal 变量
  10. static ThreadLocal<String> localVariable = new ThreadLocal<>();
  11. public static void mainString[] args {
  12. //(3) 创建线程 one
  13. Thread threadOne = new Thread(new Runnable() {
  14. public void run() {
  15. //3.1 设置线程 One 中本地变量 localVariable 的值
  16. localVariable.set(「threadOne local variable」);
  17. //3.2 调用打印函数
  18. print(「threadOne」);
  19. //3.3 打印本地变量值
  20. System.out.println(「threadOne remove after + 「:」 +localVariable.get());
  21. }
  22. });
  23. //(4) 创建线程 two
  24. Thread threadTwo = new Thread(new Runnable() {
  25. public void run() {
  26. //4.1 设置线程 Two 中本地变量 localVariable 的值
  27. localVariable.set(「threadTwo local variable」);
  28. //4.2 调用打印函数
  29. print(「threadTwo」);
  30. //4.3 打印本地变量值
  31. System.out.println(「threadTwo remove after + 「:」 +localVariable.get());
  32. }
  33. });
  34. //(5)启动线程
  35. threadOne.start();
  36. threadTwo.start();
  37. }

运行结果如下。

  1. threadOne:threadOne local variable
  2. threadTwo:threadTwo local variable
  3. threadOne remove after:threadOne local variable
  4. threadTwo remove after:threadTwo local variable

代码(2)创建了一个 ThreadLocal 变量。

代码(3)和(4)分别创建了线程 One 和 Two。

代码(5)启动了两个线程。

线程 One 中的代码 3.1 通过 set 方法设置了 localVariable 的值,这其实设置的是线程 One 本地内存中的一个副本,这个副本线程 Two 是访问不了的。然后代码 3.2 调用了 print 函数,代码 1.1 通过 get 函数获取了当前线程(线程 One)本地内存中 localVariable 的值。

线程 Two 的执行类似于线程 One。

打开代码 1.2 的注释后,再次运行,运行结果如下。

  1. threadOne:threadOne local variable
  2. threadOne remove after:null
  3. threadTwo:threadTwo local variable
  4. threadTwo remove after:null

ThreadLocal 的实现原理

首先看一下 ThreadLocal 相关类的类图结构,如图 1-5 所示。

ThreadLocal - 图3

图 1-5

由该图可知,Thread 类中有一个 threadLocals 和一个 inheritableThreadLocals,它们都是 ThreadLocalMap 类型的变量,而 ThreadLocalMap 是一个定制化的 Hashmap。在默认情况下,每个线程中的这两个变量都为 null,只有当前线程第一次调用 ThreadLocal 的 set 或者 get 方法时才会创建它们。其实每个线程的本地变量不是存放在 ThreadLocal 实例里面,而是存放在调用线程的 threadLocals 变量里面。也就是说,ThreadLocal 类型的本地变量存放在具体的线程内存空间中。ThreadLocal 就是一个工具壳,它通过 set 方法把 value 值放入调用线程的 threadLocals 里面并存放起来,当调用线程调用它的 get 方法时,再从当前线程的 threadLocals 变量里面将其拿出来使用。如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的 threadLocals 变量里面,所以当不需要使用本地变量时可以通过调用 ThreadLocal 变量的 remove 方法,从当前线程的 threadLocals 里面删除该本地变量。另外,Thread 里面的 threadLocals 为何被设计为 map 结构?很明显是因为每个线程可以关联多个 ThreadLocal 变量。

下面简单分析 ThreadLocal 的 set、get 及 remove 方法的实现逻辑。

1.void set(T value)

  1. public void set(T value) {
  2. //(1)获取当前线程
  3. Thread t = Thread.currentThread();
  4. //(2)将当前线程作为 key,去查找对应的线程变量,找到则设置
  5. ThreadLocalMap map = getMapt);
  6. if map ! = null
  7. map.setthis, value);
  8. else
  9. //(3)第一次调用就创建当前线程对应的 HashMap
  10. createMapt, value);
  11. }

代码(1)首先获取调用线程,然后使用当前线程作为参数调用 getMap(t)方法,getMap(Thread t)的代码如下。

  1. ThreadLocalMap getMap(Thread t) {
  2. return t.threadLocals;
  3. }

可以看到,getMap(t)的作用是获取线程自己的变量 threadLocals,threadlocal 变量被绑定到了线程的成员变量上。

如果 getMap(t)的返回值不为空,则把 value 值设置到 threadLocals 中,也就是把当前变量值放入当前线程的内存变量 threadLocals 中。threadLocals 是一个 HashMap 结构,其中 key 就是当前 ThreadLocal 的实例对象引用,value 是通过 set 方法传递的值。

如果 getMap(t)返回空值则说明是第一次调用 set 方法,这时创建当前线程的 threadLocals 变量。下面来看 createMap(t, value)做什么。

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

它创建当前线程的 threadLocals 变量。

2.T get()

  1. public T get() {
  2. //(4) 获取当前线程
  3. Thread t = Thread.currentThread();
  4. //(5)获取当前线程的 threadLocals 变量
  5. ThreadLocalMap map = getMapt);
  6. //(6)如果 threadLocals 不为 null,则返回对应本地变量的值
  7. if map ! = null {
  8. ThreadLocalMap.Entry e = map.getEntrythis);
  9. if e ! = null {
  10. @SuppressWarnings(「unchecked」)
  11. T result = Te.value
  12. return result
  13. }
  14. }
  15. //(7)threadLocals 为空则初始化当前线程的 threadLocals 成员变量
  16. return setInitialValue();
  17. }

代码(4)首先获取当前线程实例,如果当前线程的 threadLocals 变量不为 null,则直接返回当前线程绑定的本地变量,否则执行代码(7)进行初始化。setInitialValue()的代码如下。

  1. private T setInitialValue() {
  2. //(8)初始化为 null
  3. T value = initialValue();
  4. Thread t = Thread.currentThread();
  5. ThreadLocalMap map = getMapt);
  6. //(9)如果当前线程的 threadLocals 变量不为空
  7. if map ! = null
  8. map.setthis, value);
  9. else
  10. //(10)如果当前线程的 threadLocals 变量为空
  11. createMapt, value);
  12. return value
  13. }
  14. protected T initialValue() {
  15. return null
  16. }

如果当前线程的 threadLocals 变量不为空,则设置当前线程的本地变量值为 null,否则调用 createMap 方法创建当前线程的 createMap 变量。

3.void remove()

  1. public void remove() {
  2. ThreadLocalMap m = getMap(Thread.currentThread());
  3. if (m ! = null)
  4. m.remove(this);
  5. }

如以上代码所示,如果当前线程的 threadLocals 变量不为空,则删除当前线程中指定 ThreadLocal 实例的本地变量。

总结:如图 1-6 所示,在每个线程内部都有一个名为 threadLocals 的成员变量,该变量的类型为 HashMap,其中 key 为我们定义的 ThreadLocal 变量的 this 引用,value 则为我们使用 set 方法设置的值。每个线程的本地变量存放在线程自己的内存变量 threadLocals 中,如果当前线程一直不消亡,那么这些本地变量会一直存在,所以可能会造成内存溢出,因此使用完毕后要记得调用 ThreadLocal 的 remove 方法删除对应线程的 threadLocals 中的本地变量。在高级篇要讲解的 JUC 包里面的 ThreadLocalRandom,就是借鉴 ThreadLocal 的思想实现的,后面会具体讲解。

ThreadLocal - 图4

图 1-6

ThreadLocal 不支持继承性

首先看一个例子。

  1. public class TestThreadLocal {
  2. //(1)创建线程变量
  3. public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
  4. public static void mainString[] args {
  5. //(2) 设置线程变量
  6. threadLocal.set(「hello world」);
  7. //(3) 启动子线程
  8. Thread thread = new Thread(new Runnable() {
  9. public void run() {
  10. //(4) 子线程输出线程变量的值
  11. System.out.println(「thread:」 + threadLocal.get());
  12. }
  13. });
  14. thread.start();
  15. //(5) 主线程输出线程变量的值
  16. System.out.println(「main:」 + threadLocal.get());
  17. }
  18. }

输出结果如下。

  1. main:hello world
  2. thread:null

也就是说,同一个 ThreadLocal 变量在父线程中被设置值后,在子线程中是获取不到的。根据上节的介绍,这应该是正常现象,因为在子线程 thread 里面调用 get 方法时当前线程为 thread 线程,而这里调用 set 方法设置线程变量的是 main 线程,两者是不同的线程,自然子线程访问时返回 null。那么有没有办法让子线程能访问到父线程中的值?答案是有。

InheritableThreadLocal 类

为了解决上节提出的问题,InheritableThreadLocal 应运而生。InheritableThreadLocal 继承自 ThreadLocal,其提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量。下面看一下 InheritableThreadLocal 的代码。

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

由如上代码可知,InheritableThreadLocal 继承了 ThreadLocal,并重写了三个方法。由代码(3)可知,InheritableThreadLocal 重写了 createMap 方法,那么现在当第一次调用 set 方法时,创建的是当前线程的 inheritableThreadLocals 变量的实例而不再是 threadLocals。由代码(2)可知,当调用 get 方法获取当前线程内部的 map 变量时,获取的是 inheritableThreadLocals 而不再是 threadLocals。

综上可知,在 InheritableThreadLocal 的世界里,变量 inheritableThreadLocals 替代了 threadLocals。

下面我们看一下重写的代码(1)何时执行,以及如何让子线程可以访问父线程的本地变量。这要从创建 Thread 的代码说起,打开 Thread 类的默认构造函数,代码如下。

  1. public ThreadRunnable target {
  2. init(null target Thread-」 + nextThreadNum(), 0);
  3. }
  4. private void initThreadGroup g Runnable target String name
  5. long stackSize AccessControlContext acc {
  6. ...
  7. //(4)获取当前线程
  8. Thread parent = currentThread();
  9. ...
  10. //(5)如果父线程的 inheritableThreadLocals 变量不为 null
  11. if parent.inheritableThreadLocals = null
  12. //(6)设置子线程中的 inheritableThreadLocals 变量
  13. this.inheritableThreadLocals =
  14. ThreadLocal.createInheritedMapparent.inheritableThreadLocals);
  15. this.stackSize = stackSize
  16. tid = nextThreadID();
  17. }

如上代码在创建线程时,在构造函数里面会调用 init 方法。代码(4)获取了当前线程(这里是指 main 函数所在的线程,也就是父线程),然后代码(5)判断 main 函数所在线程里面的 inheritableThreadLocals 属性是否为 null,前面我们讲了 InheritableThreadLocal 类的 get 和 set 方法操作的是 inheritableThreadLocals,所以这里的 inheritableThreadLocal 变量不为 null,因此会执行代码(6)。下面看一下 createInheritedMap 的代码。

  1. static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
  2. return new ThreadLocalMap(parentMap);
  3. }

可以看到,在 createInheritedMap 内部使用父线程的 inheritableThreadLocals 变量作为构造函数创建了一个新的 ThreadLocalMap 变量,然后赋值给了子线程的 inheritableThreadLocals 变量。下面我们看看在 ThreadLocalMap 的构造函数内部都做了什么事情。

  1. private ThreadLocalMapThreadLocalMap parentMap {
  2. Entry[] parentTable = parentMap.table
  3. int len = parentTable.length
  4. setThresholdlen);
  5. table = new Entry[len];
  6. for int j = 0; j < len; j++) {
  7. Entry e = parentTable[j];
  8. if e ! = null {
  9. @SuppressWarnings(「unchecked」)
  10. ThreadLocal<Object> key = ThreadLocal<Object>) e.get();
  11. if key = null {
  12. //(7)调用重写的方法
  13. Object value = key.childValuee.value); //返回 e.value
  14. Entry c = new Entrykey value);
  15. int h = key.threadLocalHashCode & len -1);
  16. while table[h] = null
  17. h = nextIndexh len);
  18. table[h] = c
  19. size++;
  20. }
  21. }
  22. }
  23. }

在该构造函数内部把父线程的 inheritableThreadLocals 成员变量的值复制到新的 ThreadLocalMap 对象中,其中代码(7)调用了 InheritableThreadLocal 类重写的代码(1)。

总结:InheritableThreadLocal 类通过重写代码(2)和(3)让本地变量保存到了具体线程的 inheritableThreadLocals 变量里面,那么线程在通过 InheritableThreadLocal 类实例的 set 或者 get 方法设置变量时,就会创建当前线程的 inheritableThreadLocals 变量。当父线程创建子线程时,构造函数会把父线程中 inheritableThreadLocals 变量里面的本地变量复制一份保存到子线程的 inheritableThreadLocals 变量里面。

把 1.11.3 节中的代码(1)修改为

  1. //(1) 创建线程变量
  2. public static ThreadLocal<String> threadLocal = new InheritableThreadLocal<Stri ng>();

运行结果如下。

  1. thread:hello world
  2. main:hello world

可见,现在可以从子线程正常获取到线程变量的值了。

那么在什么情况下需要子线程可以获取父线程的 threadlocal 变量呢?情况还是蛮多的,比如子线程需要使用存放在 threadlocal 变量中的用户登录信息,再比如一些中间件需要把统一的 id 追踪的整个调用链路记录下来。其实子线程使用父线程中的 threadlocal 方法有多种方式,比如创建线程时传入父线程中的变量,并将其复制到子线程中,或者在父线程中构造一个 map 作为参数传递给子线程,但是这些都改变了我们的使用习惯,所以在这些情况下 InheritableThreadLocal 就显得比较有用。