1. ThreadLocal

ThreadLocal是一个线程内部的数据存储类,可以在指定线程中存储数据,且只有在该指定线程中才可以获取存储数据。

  • ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组 table。
  • ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。

1.1. 作用

  • 线程隔离:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用。
  • 传递数据:减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

1.2. 使用场景

某个数据是以线程为作用域且不同线程具有不同的 Lopper

如果不采取 ThreadLocal,那么系统就必须提供一个全局的哈希表来 Handler 查找指定线程的 Lopper,这样一来就必须提供一个类似于 LooperManager 的类。

复杂逻辑下的对象传递(如监听器的传递)

有时一个线程中的任务过于复杂,可能表现为函数调用栈比较深以及代码入口的多样性,这时又要监听器能够贯穿整个线程的执行过程。

如果采用 ThreadLocal 可以让监听器作为线程内的全局对象而存在,在线程内部只要通过 get 方法就可以获取监听器。

如果不采取 ThreadLocal,就只能采用另外两种办法:

  • 讲监听器作为参数的形式在函数调用栈中传递:函数调用栈越深,越容易混乱。
  • 将监听器作为静态变量供线程访问:不具有可扩展性,有几个线程在调用,就要提供几个静态监听器对象。

1.3. 示例代码

  1. // 创建 Boolean 类型的 ThreadLocal 对象
  2. ThreadLocal<Boolean> mBooleanThread = new ThreadLocal<Boolean>();
  3. mBooleanThread.set(true); // 主线程中设置为 true
  4. mBooleanThread.get(); // 主线程中获取为 true
  5. new Thread("Thread #1") {
  6. @Override
  7. public void run() {
  8. mBooleanThread.set(false); // 子线程1中设置为 false
  9. mBooleanThread.get(); // 子线程1中获取为 false
  10. }
  11. }.start();
  12. new Thread("Thread #2") {
  13. @Override
  14. public void run() { // 子线程2中不去设置
  15. mBooleanThread.get(); // 子线程2中获取为 null
  16. }
  17. }.start();

从 ThreadLocal 的 set()get() 方法可以看出,他们所操作的对象都是当前线程的 localValues 对象和 table 数组,因此在不同线程中访问同一个 ThreadLocal 的 set()get() 方法,它们对 ThreadLocal 所做的读写操作仅限于各自内部,这就是为什么 ThreadLocal 可以在多个线程找那个互不干扰的存储和修改数据。


2. ThreadLocal 原理解析

2.1. 内部设计

2.1.1. 早期方案

每个 ThreadLocal 都创建一个 ThreadLocalMap,用 Thread 作为 Map 的key,要存储的局部变量作为 Map 的 value。

ThreadLocal - 图1

2.1.2. JAVA 8 方案

每个 Thread 维护一个 ThreadLocalMap,用 ThreadLocal 实例本身 Map 的 key,要存储的局部变量作为 Map 的 value。

  • 每个 Thread 线程内部都有一 个Map(ThreadLocalMap)
  • Map 里面存储 ThreadLocal 对象( key)和线程的变量副本(value)
  • Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值。
  • 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

ThreadLocal - 图2

【优点】

  • 每个Map所存储的元素数量变少了。
  • 当Thread销毁时,ThreadLocalMap也被销毁,减少内存。

2.2. ThreadLocal 核心方法源码

方法声明 描述
protected T initialValue() 返回当前线程局部变量的初始值
public void set(T value) 设置当前线程绑定的局部变量
public T get() 获取当前线程绑定的局部变量
public void remove() 移除当前线程绑定的局部变量

2.2.1. set 方法

先获取当前线程的 ThreadLocalMap 变量,如果存在则设置值,不存在则创建并设置值。

  1. // 设置当前线程绑定的局部变量
  2. public void set(T value) {
  3. // 获取当前线程对象
  4. Thread t = Thread.currentThread();
  5. // 获取此线程对象所维护的ThreadLocalMap对象
  6. ThreadLocalMap map = getMap(t);
  7. if (map != null) {
  8. // map不为空,则设置或更新值
  9. map.set(this, value);
  10. } else {
  11. // map 为空,则为线程t创建一个ThreadLocalMap对象,并把value存放其中
  12. createMap(t, value);
  13. }
  14. }
  15. // 获取线程所维护的ThreadLocalMap对象
  16. ThreadLocalMap getMap(Thread t) {
  17. return t.threadLocals;
  18. }
  19. // 为线程创建一个ThreadLocalMap对象,并赋予初始值
  20. void createMap(Thread t, T firstValue) {
  21. // 这里的this是调用此方法的ThreadLocal对象
  22. t.threadLocals = new ThreadLocalMap(this, firstValue);
  23. }

2.2.2. get 方法

先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值 null

  1. // 获取当前线程绑定的局部变量
  2. public T get() {
  3. // 获取当前线程对象
  4. Thread t = Thread.currentThread();
  5. // 获取此线程对象所维护的ThreadLocalMap对象
  6. ThreadLocalMap map = getMap(t);
  7. if (map != null) {
  8. // 不为空,则获取值。WeakReference
  9. ThreadLocalMap.Entry e = map.getEntry(this);
  10. if (e != null) {
  11. @SuppressWarnings("unchecked")
  12. // 强转并返回value
  13. T result = (T)e.value;
  14. return result;
  15. }
  16. }
  17. // map不存在 或 map 里面没有与当前ThreadLocal关联的值
  18. return setInitialValue();
  19. }
  20. // 设置初始值
  21. private T setInitialValue() {
  22. T value = initialValue();
  23. Thread t = Thread.currentThread();
  24. ThreadLocalMap map = getMap(t);
  25. if (map != null) {
  26. // map不为空,则设置或更新值
  27. map.set(this, value);
  28. } else {
  29. // map为空,则为线程t创建一个ThreadLocalMap对象,并把value存放其中
  30. createMap(t, value);
  31. }
  32. if (this instanceof TerminatingThreadLocal) {
  33. TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
  34. }
  35. return value;
  36. }
  37. // 返回初始值
  38. // 延迟调用,在调用set方法之前先调用get方法时才会执行。
  39. protected T initialValue() {
  40. // 如果想返回除null之外的其他初始值,可以重写此方法
  41. return null;
  42. }

2.2.3. remove方法

先获取当前线程的 ThreadLocalMap 变量,如果存在则移除。

  1. // 移除当前线程绑定的局部变量
  2. public void remove() {
  3. // 获取当前线程对象所维护的ThreadLocalMap对象
  4. ThreadLocalMap m = getMap(Thread.currentThread());
  5. if (m != null) {
  6. // map不为空,则移除对应的实体
  7. m.remove(this);
  8. }
  9. }

3. ThreadLocalMap 源码分析

ThreadLocalMap 是 ThreadLocal 的静态内部类,并没有实现 Map 接口。

3.1. ThreadLocalMap 类的基本结构

ThreadLocal - 图3

3.1.1. 成员变量

  1. // 初始容量,必须是2的幂
  2. private static final int INITIAL_CAPACITY = 16;
  3. // 用于存放数据的table,长度必须是2的幂
  4. private Entry[] table;
  5. // 数组里面元素的个数,用于判断table的当前使用量是否超过阈值
  6. private int size = 0;
  7. // 进行扩容的阈值,当使用量大于它是就要进行扩容
  8. private int threshold; // Default to 0

3.1.2. 存储元素

  1. // 继承自WeakReference,将ThreadLocal对象的生命周期与线程的生命周期解绑
  2. // 如果key为null(entry.get() == null)则表示key不再被引用了,此时entry也可以从table中清除掉
  3. static class Entry extends WeakReference<ThreadLocal<?>> {
  4. Object value;
  5. // 只能使用ThreadLocal作为key,来存储K-V结构的数据
  6. Entry(ThreadLocal<?> k, Object v) {
  7. super(k);
  8. value = v;
  9. }
  10. }

3.2. ThreadLocal 内存泄漏

3.2.1. 当key是强引用

ThreadLocalMap 中的 key 使用了强引用,会导致 threadLocal 和 value 出现内存泄漏。

ThreadLocal - 图4

  • 假设在业务代码中使完 ThreadLocal,threadLocalRef被回收了。
  • 由于 threadLocalMap 的 Entry 强引用了 threadLocal,造成 threadLocal 无法被回收。
  • 在没有手动删除这个 Entry 以及 CurrentThread 依然运行的前提下,始终有引用链 threadRef -> currentThread -> threadLocalMap -> entry,Entry就不会被回收,导致Entry内存泄漏(threadLocal 和 value 同时出现内存泄漏)。

3.2.2. 当key是弱引用

ThreadLocalMap 中的 key 使用了弱引用,会导致 value 出现内存泄漏。

ThreadLocal - 图5

  • 假设在业务代码中使完 ThreadLocal,threadLocalRef被回收了。
  • 由于 ThreadLocalMap 只持有 ThreadLocal 的弱引用,没有任何强引用指向 threadlocal 实例,所以 threadlocal 就可以顺利被gc回收,此时 Entry 中的 key=null。
  • 在没有手动删除这个 Entry 以及 CurrentThread 依然运行的前提下,也存在有强引用链 threadRef -> currentThread -> threadLocalMap -> entry -> value,value不会被回收,而这块 value 永远不会被访问到了,导致 value 内存泄漏。

3.2.3. 导致内存泄漏的原因

  • 没有手动删除相应的Entry对象
  • 当前线程依然在运行

【解决办法】

  • 使用完 ThreadLocal,调用其 remove 方法删除对应的 Entry。
  • 使用完 ThreadLocal,当前 Thread 也随之运行结束。(不好控制,线程池中的核心线程不会销毁)

3.3. 解决Hash冲突

3.3.1. ThreadLocalMap 的构造方法

  1. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  2. // 初始化table数组,长度为16
  3. table = new Entry[INITIAL_CAPACITY];
  4. // 计算索引【重点分析】
  5. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  6. // 在相应索引位置存放对应的值
  7. table[i] = new Entry(firstKey, firstValue);
  8. size = 1;
  9. // 设置阈值
  10. setThreshold(INITIAL_CAPACITY);
  11. }

【firstKey.threadLocalHashCode】

  1. // 这个值跟斐波那契数列(黄金分割数)有关。
  2. // 目的是让哈希码能均匀的分布在2的n次方的数组里,即Entry[],可以尽量避免hash冲突。
  3. private static final int HASH_INCREMENT = 0x61c88647;
  4. private final int threadLocalHashCode = nextHashCode();
  5. private static AtomicInteger nextHashCode = new AtomicInteger();
  6. private static int nextHashCode() {
  7. // 每次获取值时,把当前值加上HASH_INCREMENT并返回
  8. return nextHashCode.getAndAdd(HASH_INCREMENT);
  9. }

【& (INITIAL_CAPACITY - 1)】

计算 hash 时,采用 hashCode & (size - 1) 的算法,是取模运算 hashCode % size 的高效实现。

正因为这种算法,要求 size 必须是2的整次幂,保证在索引不越界的前提下,使得 hash 发生冲突的次数减小。

3.3.2. ThreadLocalMap 的 set 方法

  1. private void set(ThreadLocal<?> key, Object value) {
  2. Entry[] tab = table;
  3. int len = tab.length;
  4. // 计算索引【重点分析】
  5. int i = key.threadLocalHashCode & (len-1);
  6. // 使用线性探测法查找元素【重点分析】
  7. for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
  8. ThreadLocal<?> k = e.get();
  9. // 对应的key存在,则直接覆盖旧的值
  10. if (k == key) {
  11. e.value = value;
  12. return;
  13. }
  14. // key为null,值不为null,说明之前的ThreadLocal对象已经被回收了
  15. if (k == null) {
  16. // 使用新的元素替换旧的元素
  17. replaceStaleEntry(key, value, i);
  18. return;
  19. }
  20. }
  21. // 探测完毕都没找到,则在空元素位置创建一个新的Entry
  22. tab[i] = new Entry(key, value);
  23. // 叠加一个数量
  24. int sz = ++size;
  25. // cleanSomeSlots用于清除那些e.get())==null的元素
  26. // 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null.
  27. // 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行
  28. // rehash (执行一次全表的扫描清理工作)
  29. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  30. rehash();
  31. }
  32. // 获取环形数组的下一个索引
  33. private static int nextIndex(int i, int len) {
  34. // 如果当前索引+1之后越界了,则返回索引0
  35. return ((i + 1 < len) ? i + 1 : 0);
  36. }

【执行流程】

  1. 根据key计算出索引i,然后查找i位置上的Entry。
  2. 如果Entry存在并且key等于传入的key,则直接给这个Entry赋新的value值。
  3. 如果Entry存在但是key等于null,则调用replaceStaleEntry来更换这个keynull的Entry。
  4. 循环检测,直到遇到为null的地方,此时如果循环还没有return,就在这个null的位置新建一个Entry,并且插入,同时size增加1。
  5. 最后调用cleanSomeSlots,清理keynull的Entry。
  6. 返回是否清理了Entry,接下来再判断size是否达到了rehash的条件(size≥thresgold),达到的话就会调用rehash函数执行一次全表的扫描清理。

【线性探测法】

用来解决哈希冲突。

  • 依次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。
  • 举例,假设当前table长度为16,计算出来key的hash值为14,如果table[14]上已经有值,且其key与当前key不一致,那么就发生了hash冲突,这时将14加1得到15,取table[15]进行判断,如果还冲突会回到0,取table[0],以此类推直到可以插入为止。
  • 按照上面的描述,可以把Entry[] table看成一个环形数组。