前言

ThreadLocal直译为线程局部变量,或许将它命名为ThreadLocalVariable更为合适。其主要作用就是实现线程本地存储功能,通过线程本地资源隔离,解决多线程并发场景下线程安全问题。

ThreadLocal

接下来,通过ThreadLocal的使用案例、应用场景、源码分析来进行深层次的剖析,说明如何避免使用中出现问题以及解决方案。

使用案例

前面提到关于ThreadLocal的线程隔离性,通过下面一个简单的例子来演示ThreadLocal的隔离性。

  1. package com.starsray.test.tl;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. public class ThreadLocalTest {
  5. // 声明一个ThreadLocal成员变量
  6. private final ThreadLocal<Person> tl = new ThreadLocal<>();
  7. // 声明一个List作为参照对象
  8. private final List<Person> list = new ArrayList<>();
  9. public static void main(String[] args) {
  10. new ThreadLocalTest().test();
  11. }
  12. public void test() {
  13. // 创建测试Person对象
  14. Person person = new Person();
  15. person.setName("张三");
  16. person.setAge(24);
  17. // 创建线程一:再启动1s后,分别添加person对象到tl、list对象中
  18. new Thread(() -> {
  19. try {
  20. Thread.sleep(1000);
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. tl.set(person);
  25. list.add(person);
  26. System.out.println(Thread.currentThread().getName() + " [thread] get():" + tl.get());
  27. System.out.println(Thread.currentThread().getName() + " [list] get():" + list.get(0));
  28. },"thread-1").start();
  29. // 创建线程二:在启动2s后,分别去tl、list对象中取person对象
  30. new Thread(() -> {
  31. try {
  32. Thread.sleep(2000);
  33. } catch (InterruptedException e) {
  34. e.printStackTrace();
  35. }
  36. System.out.println(Thread.currentThread().getName() + " [thread] get():" + tl.get());
  37. System.out.println(Thread.currentThread().getName() + " [list] get():" + list.get(0));
  38. },"thread-2").start();
  39. }
  40. // 测试静态内部类Person
  41. static class Person {
  42. private String name;
  43. private int age;
  44. public void setName(String name) {
  45. this.name = name;
  46. }
  47. public void setAge(int age) {
  48. this.age = age;
  49. }
  50. @Override
  51. public String toString() {
  52. return "Person{" +
  53. "name='" + name + '\'' +
  54. ", age=" + age +
  55. '}';
  56. }
  57. }
  58. }

案例中使用两个线程同时对List和ThreadLocal对象进行操作,通过对照实验,从输出结果可以看到,ThreadLocal实现了线程间数据隔离,这也说明每一个Thread对象维护了自己的一份数据。

  1. hread-1 [thread] get():Person{name='张三', age=24}
  2. thread-1 [list] get():Person{name='张三', age=24}
  3. thread-2 [thread] get():null
  4. thread-2 [list] get():Person{name='张三', age=24}

应用场景

针对ThreadLocal而言,由于其适合隔离、线程本地存储等特性,因此天然的适合一些Web应用场景,比如下面所列举的例子:

  • 代替参数显式传递(很少使用)
  • 存储全局用户登录信息
  • 存储数据库连接,以及Session等信息
  • Spring事务处理方案

    源码分析

    通过使用案例的展示,接下来对ThreadLocal的实现原理进行简单分析。

    WeakReference

    在对ThreadLocal的源码展开描述之前,首先简单提一下Java中四种引用类型,强、软、若、虚之一的弱引用,这四种引用关系引用程度依次降低。Java中弱引用通过WeakReference表示,在JDK1.2引入。

    1. public class WeakReference<T> extends Reference<T> {
    2. // 创建一个给定类型的对象弱引用
    3. public WeakReference(T referent) {
    4. super(referent);
    5. }
    6. // 创建一个给定类型的对象弱引用,并注册到队列
    7. public WeakReference(T referent, ReferenceQueue<? super T> q) {
    8. super(referent, q);
    9. }
    10. }

    弱引用用来描述非必须对象,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。如果发生垃圾收集,无论内存空间是否满足,都会回收掉被弱引用关联的对象。
    例如下面代码模拟,为了便于模拟出效果,指定虚拟机启动参数:-Xms4m -Xmx4m

    1. public class WeakRefTest {
    2. @Override
    3. protected void finalize() {
    4. System.out.println("gc");
    5. }
    6. public static void main(String[] args) {
    7. for (int i = 0; i < 500; i++) {
    8. WeakRefTest weakRefTest = new WeakRefTest();
    9. new WeakReference<>(weakRefTest);
    10. if (i >= 450) {
    11. System.gc();
    12. }
    13. }
    14. }
    15. }
  • finalize()是Object方法,当虚拟机在回收对象时,允许执行完该方法后再进行回收

  • System.gc()会通知虚拟机进行垃圾回收,并不会立即进行垃圾回收

执行结果:

  1. gc
  2. ...

关于弱引用的特性,为什么ThreadLocal中要使用弱引用来维护一个对象,后面会继续进行描述。

ThreadLocalMap

ThreadLocalMap是ThreadLocal的一个静态内部类。每一个Thread对象实例中都维护了ThreadLocalMap对象,对象本质存储了一组以ThreadLocal为key(this对象实际使用的是唯一threadLocalHashCode值),以本地线程包含变量为value的K-V键值对。
在ThreadLocalMap内部还维护了一个Entry静态内部类,该类继承了WeakReference,并指定其所引用的泛型类为ThreadLocal类型。Entry是一个键值对结构,使用ThreadLocal类型对象作为引用的key。

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

查看Entry源码。Entry之所以使用数组结构,一个Thread在运行的过程中会存在多个ThreadLocal对象的场景,ThreadLocalMap作为ThreadLocal的静态内部类,需要维护多个ThreadLocal对象所存储的value值。

  1. // 初始化默认容量为 16
  2. private static final int INITIAL_CAPACITY = 16;
  3. // 数据存储结构底层实现为Entry数组,其长度必须为2的倍数
  4. private Entry[] table;
  5. // table中Entry的实际数量,初始值为0
  6. private int size = 0;
  7. // 存储的阈值
  8. private int threshold; // Default to 0
  9. // resize扩容阈值加载因子为2/3
  10. private void setThreshold(int len) {
  11. threshold = len * 2 / 3;
  12. }

整个ThreadLocal类中核心内容都是对ThreadLocalMap进行操作,而ThreadLocalMap的核心内容都是围绕Entry组成的Map存储结构进行操作。关于ThreadLocal、ThreadLocalMap、Entry之间的关系如图所示:
ThreadLocal.png
ThreadLocal对象是当前线程的ThreadLocalMap的访问入口,Thread类中维护了两个关于ThreadLocalMap的成员变量。

  1. // ThreadLocal变量
  2. ThreadLocal.ThreadLocalMap threadLocals = null;
  3. // InheritableThreadLocal变量,该类继承自ThreadLocal
  4. ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

threadLocals在Thread类中作为成员变量,初始化线程对象时并不会被赋予值,只有在使用ThreadLocal时进行赋值。查看ThreadLocal中的get方法

  1. public T get() {
  2. // 获取当前操作线程
  3. Thread t = Thread.currentThread();
  4. // 调用getMap方法,返回当前线程的实例变量threadLocals值
  5. ThreadLocalMap map = getMap(t);
  6. // 如果返回map不为空,返回map中所存储的以当前ThreadLocal对象为key的值
  7. if (map != null) {
  8. ThreadLocalMap.Entry e = map.getEntry(this);
  9. if (e != null) {
  10. @SuppressWarnings("unchecked")
  11. T result = (T)e.value;
  12. return result;
  13. }
  14. }
  15. // 如果map为空进行map值的初始化
  16. return setInitialValue();
  17. }
  18. ThreadLocalMap getMap(Thread t) {
  19. // 返回传入线程(当前线程)中成员变量的threadLocals值
  20. return t.threadLocals;
  21. }
  22. private T setInitialValue() {
  23. // 调用initialValue()方法设置初始值,默认不设置任何值,可以在创建ThreadLocal
  24. // 对象时被重写进行初始化,只会进行一次初始化。
  25. T value = initialValue();
  26. Thread t = Thread.currentThread();
  27. ThreadLocalMap map = getMap(t);
  28. if (map != null)
  29. map.set(this, value);
  30. else
  31. createMap(t, value);
  32. return value;
  33. }
  34. void createMap(Thread t, T firstValue) {
  35. // 初始化当前线程对象实例变量threadLocals的值,Map所对应的key为当前ThreadLocal对象
  36. t.threadLocals = new ThreadLocalMap(this, firstValue);
  37. }

接下来查看set方法

  1. public void set(T value) {
  2. // 获取当前线程对象
  3. Thread t = Thread.currentThread();
  4. // 调用getMap方法,传入当前对象的值,获取当前线程的实例变量threadLocals值
  5. ThreadLocalMap map = getMap(t);
  6. if (map != null)
  7. map.set(this, value);
  8. else
  9. // 如果map为空,创建ThreadLocalMap
  10. createMap(t, value);
  11. }

而inheritableThreadLocals会在创建线程时,根据线程构造方法传参,确定是否进行初始化。

  1. // 该init方法为Thread内部线程初始化方法,inheritThreadLocals是否继承父类变量,默认false
  2. private void init(ThreadGroup g, Runnable target, String name,
  3. long stackSize, AccessControlContext acc,
  4. boolean inheritThreadLocals);
  5. // 如果inheritThreadLocals为true并且parent(为当前线程,视为要被继承线程的父线程)
  6. // 的ThreadLocal不为null,调用createInheritedMap方法进行继承初始化
  7. if (inheritThreadLocals && parent.inheritableThreadLocals != null)
  8. this.inheritableThreadLocals =
  9. ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  10. // 为子线程创建一个新的ThreadLocalMap并初始化parentMap中的变量实现继承
  11. static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
  12. return new ThreadLocalMap(parentMap);
  13. }

threadLocalHashCode

在ThreadLocal的成员变量中包含了threadLocalHashCodeHASH_INCREMENT两个成员变量:

  1. private final int threadLocalHashCode = nextHashCode();
  2. // 连续生成的哈希码之间的差异
  3. // 将隐式顺序线程本地 ID 转换为接近最佳传播的乘法哈希值,用于二次幂大小的表
  4. private static final int HASH_INCREMENT = 0x61c88647;

关于threadLocalHashCode的解释:

ThreadLocals 依赖于附加到每个线程(Thread.threadLocals 和inheritableThreadLocals)的每线程线性探针哈希映射。 ThreadLocal 对象充当键,通过 threadLocalHashCode 进行搜索。这是一个自定义哈希码(仅在 ThreadLocalMaps 中有用),它消除了在相同线程使用连续构造的 ThreadLocals 的常见情况下的冲突,同时在不太常见的情况下保持良好行为。

HASH_INCREMENT是用来计算下一个哈希码(threadLocalHashCode)的哈希魔数,源码中用十六进制表示,其对应的十进制数为1640531527,至于为什么是这个数值并不是偶然,而是这个数在有符号的int范围内是黄金分割数。
如下代码所示,输出的结果刚好是-1640531527,也就是说是32位有符号整数的黄金分割值。

  1. public static void main(String[] args) {
  2. long c = (long) ((1L << 32) * (Math.sqrt(5) - 1) / 2);
  3. System.out.println(c);
  4. //强制转换为带符号为的32位整型,值为-1640531527
  5. int i = (int) c;
  6. System.out.println(i);
  7. System.out.println(Integer.MAX_VALUE);
  8. }

ThreadLocal在ThreadLocalMap中是根据ThreadLocal对象的threadLocalHashCode进行索引的,查看下面一段源码,从源码可以看到在Entry表里求下标的算法为:
哈希key:keyIndex = key.threadLocalHashCode & (table.length - 1);

  1. private Entry getEntry(ThreadLocal<?> key) {
  2. int i = key.threadLocalHashCode & (table.length - 1);
  3. Entry e = table[i];
  4. if (e != null && e.get() == key)
  5. return e;
  6. else
  7. return getEntryAfterMiss(key, i, e);
  8. }

哈希key的求值也可以看作:keyIndex = ((i + 1) * HASH_INCREMENT) & (length - 1)i为ThreadLocal实例的个数,HASH_INCREMENT是哈希魔数0x61c88647lengthThreadLocalMap中可容纳的Entry的容量。初始容量为16,扩容后总是2的幂次方。
下面做个测试为什么使用HASH_INCREMENT作为魔数,以及求取下标的算法巧妙之处:

  1. public class TestThreadLocalHashCode {
  2. public static void main(String[] args) {
  3. hashCode(4);
  4. hashCode(16);
  5. hashCode(32);
  6. hashCode(64);
  7. }
  8. private static void hashCode(int capacity) {
  9. final int HASH_INCREMENT = 0x61c88647;
  10. final AtomicInteger nextHashCode = new AtomicInteger(HASH_INCREMENT);
  11. int keyIndex;
  12. for (int i = 0; i < capacity; i++) {
  13. keyIndex = nextHashCode.getAndAdd(HASH_INCREMENT) & (capacity - 1);
  14. // keyIndex = ((i + 1) * HASH_INCREMENT) & (capacity - 1);
  15. System.out.print(keyIndex + " ");
  16. }
  17. System.out.println();
  18. }
  19. }

在不触发二次扩容的场景下,每个ThreadLocalMap中的元素分别为4,16,32,64,输出结果:

  1. 3 2 1 0
  2. 7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0
  3. 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
  4. 7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0

每组测试在进行散列算法后刚好填满了整个容器,实现了完美散列,这使得ThreadLocal在使用的过程中可以尽可能高的提高在ThreadLocalMap中获取元素的命中率,提高了ThreadLocal在使用中的效率。
此外从输出结果来看,ThreadLocal中使用了斐波那契散列法,保证哈希表的离散度。它选用的乘数值即是2^32的黄金分割比0x61c88647

注意事项

ThreadLocal提供了便利的同时当然也需要注意在使用过程中的一些细节问题。下面进行简单总结

异步调用

ThreadLocal默认情况下不会进行子线程对父线程变量的传递性,在开启异步线程的时候需要注意这一点,关于这一点可以通过Thread类构造方法提供的inheritThreadLocals参数进行封装,或者使用Spring根据装饰器模式进行封装的TaskDecorator类实现跨线程传递方法。

线程池问题

线程池中线程调用使用ThreadLocal 需要注意,由于线程池中对线程管理都是采用线程复用的方法,在线程池中线程非常难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测。另外重复使用可能导致ThreadLocal
对象未被清理,在ThreadLocalMap中进行值操作时被覆盖,或取到旧值。如下代码所示:

  1. package com.starsray.test.thread;
  2. import java.util.concurrent.ExecutorService;
  3. import java.util.concurrent.Executors;
  4. import java.util.concurrent.atomic.AtomicInteger;
  5. public class ThreadLocalPoolTest {
  6. private final static ThreadLocal<AtomicInteger> tl = ThreadLocal.withInitial(() -> new AtomicInteger(0));
  7. static class Task implements Runnable{
  8. @Override
  9. public void run() {
  10. System.out.println(tl.get().incrementAndGet());
  11. }
  12. }
  13. public static void main(String[] args) {
  14. ExecutorService pool = Executors.newFixedThreadPool(2);
  15. for (int i = 0; i < 5; i++) {
  16. pool.execute(new Task());
  17. }
  18. pool.shutdown();
  19. }
  20. }

期待的输出结果应该是1,实际输出结果,由于超出后线程被复用,输出结果也会取到旧值。

  1. 1
  2. 1
  3. 2
  4. 2
  5. 3
  6. 3

当然,如果必须要在线程池中使用ThreadLocal也不是不能使用,在线程池类ThreadPoolExecutor中定义了钩子函数,可以在初始化或者任务执行完做特殊处理,如初始化ThreadLocal或者记录日志。重写beforeExecute方法:

  1. protected void beforeExecute(Thread t, Runnable r) { }

内存泄露

ThreadLocal对象不仅提供了get、set方法,还提供了remove方法。虽然get、set已经对空值进行清理,但在实际使用时,手动调用remove方法养成良好的编程习惯是非常有必要的。
ThreadLocal中主要的存储单元Entry类继承了WeakReference,该类的引用在虚拟机进行GC时会被进行清理,但是对于value如果是强引用类型,就需要进行手动remove,避免value的内存泄露。关于引用关系参考下图所示:
未命名文件 (2).svg
关于内存泄露这块的重点在于两部分:

  • ThreadLocal被一强(tl = new强引用)一弱(WeakReference>)两部分引用,强引用可以通过编码解决(tl = null),而弱引用部分在GC时会自动清理掉key部分的引用。
  • 关于value部分的引用,如果是强引用类型的value通过remove方法可以清理,避免内存泄露。

具体细节查看remove部分的源码:

  1. public void remove() {
  2. // 获取当前线程中threadLocals对应的ThreadLocalMap
  3. ThreadLocalMap m = getMap(Thread.currentThread());
  4. if (m != null)
  5. // 如果ThreadLocalMap不为空,继续调用remove(this)方法
  6. m.remove(this);
  7. }

查看remove(this)具体内容

  1. private void remove(ThreadLocal<?> key) {
  2. // 创建新都tab数组,引用指向当前ThreadLocal对象中的table
  3. Entry[] tab = table;
  4. // tab的长度
  5. int len = tab.length;
  6. // 根据当前ThreadLocal对象的唯一threadLocalHashCode值并通过与操作
  7. // 获取当前ThreadLocal对象在table中value所在的下标值i
  8. int i = key.threadLocalHashCode & (len-1);
  9. for (Entry e = tab[i];
  10. e != null;
  11. e = tab[i = nextIndex(i, len)]) {
  12. // 对table进行遍历,如果Entry所对应的key为当前ThreadLocal对象,执行clear方法
  13. if (e.get() == key) {
  14. // 将引用置为null
  15. e.clear();
  16. // 清楚陈旧的键值对
  17. expungeStaleEntry(i);
  18. return;
  19. }
  20. }
  21. }

查看clear()方法,Clear方法位于Reference类中,由于Entry类继承了WeakReference(继承WeakReference),此处的clear属于多态的应用。

  1. public void clear() {
  2. this.referent = null;
  3. }

接下来expungStaleEntry(i)方法则是整个remove的核心逻辑了,这里首先再明确以下两个变量的意义:

  • size:前面提到size是Entry的数量,即ThreadLocal中成员变量table的实际键值对数量
  • i:table中与当前ThreadLocal对象相匹配的Entry的key值的下标

    1. private int expungeStaleEntry(int staleSlot) {
    2. // 创建新都tab数组,引用指向当前ThreadLocal对象中的table
    3. Entry[] tab = table;
    4. // tab的长度
    5. int len = tab.length;
    6. // 将tab中下标staleSlot(i)对应的value引用置为null
    7. tab[staleSlot].value = null;
    8. // 将tab中下标staleSlot的Entry置为null
    9. tab[staleSlot] = null;
    10. // Entry对应的长度减1
    11. size--;
    12. // Rehash until we encounter null 直到遇到null开始rehash
    13. Entry e;
    14. int i;
    15. // 从staleSlot后以索引开始遍历,直到遇到某个Entry不为空为止
    16. for (i = nextIndex(staleSlot, len);
    17. (e = tab[i]) != null;
    18. i = nextIndex(i, len)) {
    19. // 获取Entry对应的ThreadLocal对象的引用key值
    20. ThreadLocal<?> k = e.get();
    21. if (k == null) {
    22. // 如果为空,将value和键值对同时置空,size减1
    23. e.value = null;
    24. tab[i] = null;
    25. size--;
    26. } else {
    27. // 如果k不为null,说明弱引用未被GC回收,获取table中k对应的下标
    28. int h = k.threadLocalHashCode & (len - 1);
    29. // 判断传入下标,与当前k对象的下标是否一直
    30. if (h != i) {
    31. // 如果不一致,需要对tab中的值进行更新,直接清空
    32. tab[i] = null;
    33. // Unlike Knuth 6.4 Algorithm R, we must scan until
    34. // null because multiple entries could have been stale.
    35. while (tab[h] != null)
    36. // 采用R算法的变种,从当前h开始寻找一个为null的值存储e
    37. h = nextIndex(h, len);
    38. tab[h] = e;
    39. }
    40. }
    41. }
    42. // 返回第一个entry为null的下标
    43. return i;
    44. }

    关于expungeStaleEntry中原作者对关键地方进行了英文注释,源码提及了Knuth 6.4 Algorithm R算法,R算法主要说明了如何从使用线性探测的散列表中删除一个元素。
    与Knuth 6.4算法R不同,这里必须扫描到null,可能出现空的Entry,多个条目可能已经过时,由于不使用引用队列,因此只有在表开始空间不足时才能保证删除过时的条目。

    总结

    Thread对象中通过维护了一个ThreadLocal.ThreadLocalMap类型的threadLocals变量实现线程间变量隔离,并维护了一个ThreadLocal.ThreadLocalMap类型的inheritableThreadLocals变量实现线程间变量的继承,是否继承由线程初始化时inheritThreadLocals参数进行决定,默认不继承。
    ThreadLocal中核心存储的类为ThreadLocalMap类,ThreadLocalMap类本身是一个定制化的Map,这个Map以当前ThreadLocal对象作为key值进行K-V存储。ThreadLocalMap的初始化容量为16,扩容因子为2/3。
    ThreadLocalMap在进行存储时,会获取当前this对象的threadLocalHashCode值(这也是为什么使用ThreadLocal作为key的原因),该值是唯一的,只在ThreadLocalMap中有用,使用Unsafe提供的AtomicInt类操作获取。
    ThreadLocalMap中进行存储的基本单位为Entry数组,数组下标通过threadLocalHashCode进行&运算并根据当前数组长度进行自动扩容。

    说明:为什么ThreadLocal的key要使用当前ThreadLocal对象或者说是threadLocalHashCode的值,而不是使用当前线程对象? 一个ThreadLocal对象只会对应一个线程对象,但是一个Thread对象会存在多个ThreadLocal对象,之所以不使用Thread对象作为key,是为了避免多个ThreadLocal对象(或者说T、、hreadLocalMap)之间的互相影响。