ThreadLocal原理

发布时间 2023-04-05 17:45:34作者: 无虑的小猪

一、什么是ThreadLocal

  ThreadLocal是线程内的局部变量,仅在线程的生命周期内起作用。变量值在线程间不可见。

二、ThreadLocal的使用

  ThreadLocal使用详情如下:

 1 import java.util.concurrent.CountDownLatch;
 2 
 3 public class TestThreadLocal {
 4     public static void main(String[] args) {
 5         ThreadLocal th = new ThreadLocal();
 6         th.set(Thread.currentThread().getName());
 7         CountDownLatch countDownLatch = new CountDownLatch(3);
 8         for (int i = 0; i < 3; i++) {
 9             // 子线程
10             new Thread(() -> {
11                 try{
12                     if (th.get() == null) {
13                         th.set("current Thread Name:" + Thread.currentThread().getName());
14                     }
15                 }finally {
16                     System.out.println(th.get());
17                     countDownLatch.countDown();
18                 }
19             }).start();
20 
21         }
22         // 主线程
23         System.out.println("current Thread Name:" + th.get());
24         try {
25             countDownLatch.await();
26         } catch (InterruptedException e) {
27             e.printStackTrace();
28         }
29     }
30 }

三、ThreadLocal原理

 

  Thread中持有ThreadLocal.ThreadLocalMap容器存储线程变量,每个线程都有属于自己的ThreadLocal.ThreadLocalMap,当获取线程变量时,优先从当前的Thread中获取ThreadLocal.ThreadLocalMap,若容器不为空,则将ThreadLocal作为key,变量值作为value设置到ThreadLocal.ThreadLocalMap中,若ThreadLocal.ThreadLocalMap为空,则优先创建ThreadLocal.ThreadLocalMap对象,通过构造函数初始化ThreadLocalMap的属性。

  ThreadLocal的set/get方法,实际上调用的ThreadLocalMap的set/get方法。ThreadLocalMap内部维护Entry对象数组,数组初始容量、扩容阀值,同时拥有扩容resize()和清理无效数据expungsStaleEntries()的方法避免内存泄露。

  Entry对象用来存储ThrealLocal与变量值Value,key为ThrealLocal,Value为设置的变量值。

四、ThreadLocal源码分析

  ThreadLocal主要内部信息如下:

 

  ThreadLocal中ThreadLocalMap作为容器存储线程变量,set()、get()、remove()为添加、获取、删除线程变量的方法。

4.1、容器 - ThreadLocalMap

  ThreadLocalMap是ThreadLocal静态内部类,是一个类似HashMap的容器,用于存储线程变量,key作为线程的引用,value作为线程变量的值。

  

  ThreadLocalMap与HashMap类似,持有Entry数组来保存线程及变量值,默认数组的初始大小16,阀值为数组初始值的三分之二,默认阀值10,超过此阀值,Entry数组可进行扩容操作。

  ThreadLocalMap中提供删除过期数据的方法,防止内存泄露。

1、Entry

  Entry的类图结构如下:

 

  Java中常见的引用类型:强、软、弱、虚。弱引用 java.lang.ref.WeakReference 来表示。弱引用类型,当JVM发生gc时,会回收弱引用对象占用的空间,防止内存泄露。

Reference是所有引用对象的基类,定义了应用对象的基本操作。 
 

  Entry是弱引用类型,将当前线程的引用作为key,线程变量值作为value。Entry通过get()方法获取当前线程的ThreadLocal对象,若获取到的ThreadLocal为空,那么Entry对象会从Entry数组中移除。

 1 static class Entry extends WeakReference<ThreadLocal<?>> {
 2     // ThreadLocal中存储的变量值
 3     Object value;
 4     
 5     // 构造函数
 6     Entry(ThreadLocal<?> k, Object v) {
 7         // 当前线程,设置到父类Reference的 referent 属性
 8         super(k);
 9         // 线程变量值
10         value = v;
11     }
12 }

  Entry对象用来存储 ThreadLocal对象、线程变量值。

2、ThreadLocalMap构造函数

  ThreadLocalMap中Entry数组,用来存储线程与变量值,与HashMap的类似,ThreadLocalMap的阀值为Entry数组大小的三分之二,用来做Entry数组是否需要扩容的判断。对ThreadLocal的hashCode值进行位运算获取Entry数组的下标。

 1 // 线程变量持有对象集合,可做扩容处理
 2 private Entry[] table;
 3 // 集合初始化大小
 4 private static final int INITIAL_CAPACITY = 16;
 5 // Entry在数组中的数量
 6 private int size = 0;
 7 // 负载因子,为数组容量的 三分之二,用来做扩容判断
 8 private int threshold;
 9 
10 // 构造函数
11 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
12     // 初始化Entry数组
13     table = new Entry[INITIAL_CAPACITY];
14     // 位运算获取数组下标
15     int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
16     // 创建持有当前线程、变量值的Entry对象设置进数组中
17     table[i] = new Entry(firstKey, firstValue);
18     // Entry在数组中的数量
19     size = 1;
20     // 设置负载因子,为 INITIAL_CAPACITY 的 三分之二
21     setThreshold(INITIAL_CAPACITY);
22 }
23 
24 // 初始化
25 private void setThreshold(int len) {
26     threshold = len * 2 / 3;
27 }

3、扩容处理

  ThreadLocalMap的Entry数组,扩容后的容量是原来的2倍。在扩容前,先清理Entry数组中所有无效的Entry对象,清理完成后,再判断Entry数组中的Entry元素数量是否达到或超过阀值的四分之三,满足此条件,数组才会做扩容处理。

  在扩容阶段,如果出现hash冲突,会沿着当前下标往数组后面寻找Entry元素为null的地址,将Entry元素设置到该地址中,这种方式也叫作线性探测寻址法。

当Entry数组中的Entry对象达到或超出了设置的阀值,需要对ThreadLocalMap的Entry[]做扩容处理,ThreadLocalMap#rehash() 方法:

 1 // 扩容处理
 2 private void rehash() {
 3     // 清理Entry数组中无效的Entry对象,
 4     expungeStaleEntries();
 5 
 6     // Entry数组中的有效Entry对象 大于等于  阀值的四分之三 ,做扩容处理
 7     if (size >= threshold - threshold / 4)
 8         resize();
 9 }
10 
11 // 清除Entry数组中所有被删除的Entry对象(Entry对象的Refrent引用为null))
12 private void expungeStaleEntries() {
13     Entry[] tab = table;
14     int len = tab.length;
15     // 遍历数组
16     for (int j = 0; j < len; j++) {
17         Entry e = tab[j];
18         // Entry对象的Refrent引用为null
19         if (e != null && e.get() == null)
20             // 清除元素
21             expungeStaleEntry(j);
22     }
23 }
24 
25 // 扩容处理
26 private void resize() {
27     // 原Entry数组
28     Entry[] oldTab = table;
29     // 原Entry数组长度
30     int oldLen = oldTab.length;
31     // 新Entry数组长度
32     int newLen = oldLen * 2;
33     // 创建新的Entry数组
34     Entry[] newTab = new Entry[newLen];
35     // 统计有效的Entry对象
36     int count = 0;
37     
38     // 遍历原Entry数组
39     for (int j = 0; j < oldLen; ++j) {
40         // 获取Entry对象
41         Entry e = oldTab[j];
42         // Entry对象不为null
43         if (e != null) {
44             // 获取Entry的refrent
45             ThreadLocal<?> k = e.get();
46             if (k == null) {
47                 // 有助于gc清理
48                 e.value = null;
49             // Entry设置到新数组
50             } else {
51                 // 计算原Entry数组元素在新Entry数组中的下标
52                 int h = k.threadLocalHashCode & (newLen - 1);
53                 // hash冲突的处理,线性探测地址法
54                 while (newTab[h] != null)
55                     h = nextIndex(h, newLen);
56                 // 添加Entry元素到新数组
57                 newTab[h] = e;
58                 // Entry元素数量 + 1
59                 count++;
60             }
61         }
62     }
63     
64     // 设置扩容后的阀值
65     setThreshold(newLen);
66     // 设置有效Entry元素的个数
67     size = count;
68     // 设置Entry数组
69     table = newTab;
70 }

4.2、设置线程变量 - set源码分析

  首次调用ThreadLocal的set方法设置线程变量值,执行createMap方法创建ThreadLocalMap对象,通过有参构造方法传递当前线程引用、变量值,存储到ThreadLocalMap中。实际上是存储在ThreadLocalMap的Entry数组中。不是首次调用ThreadLocal的set方法,获取ThreadLocalMap对象,通过ThreadLocalMap的set方法将线程引用与变量值设置到ThreadLocalMap中。

  ThreadLocal线程变量添加值,ThreadLocal#set() 核心代码:

 1 // 存储当前线程与变量值的映射
 2 ThreadLocal.ThreadLocalMap threadLocals = null;
 3 
 4 public void set(T value) {
 5     // 获取当前线程
 6     Thread t = Thread.currentThread();
 7     // 获取线程与变量值映射容器
 8     ThreadLocalMap map = getMap(t);
 9     // 容器已存在,添加
10     if (map != null)
11         map.set(this, value);
12     // 首次设置线程变量值,容器不存在,创建容器存储线程与变量值的映射
13     else
14         createMap(t, value);
15 }
16 
17 // 获取与ThreadLocal关联的Map
18 ThreadLocalMap getMap(Thread t) {
19     return t.threadLocals;
20 }
21 
22 // 创建Map容器
23 void createMap(Thread t, T firstValue) {
24     // ThreadLocalMap存储当线程与变量值映射
25     t.threadLocals = new ThreadLocalMap(this, firstValue);
26 }

  设置线程变量实际调用的是ThreadLocalMap的set()方法,ThreadLocalMap#set() 核心代码:

 1 private void set(ThreadLocal<?> key, Object value) {
 2     // 获取Entry数组
 3     Entry[] tab = table;
 4     // 获取当前数组长度
 5     int len = tab.length;
 6     // 根据线程引用获取数组下标
 7     int i = key.threadLocalHashCode & (len-1);
 8     
 9     // 线性探测地址法 解决hash冲突
10     // 当前key在数组中是否存在
11     for (Entry e = tab[i];
12          e != null;
13          e = tab[i = nextIndex(i, len)]) {
14              
15         // 获取Entry对象的线程引用
16         ThreadLocal<?> k = e.get();
17         // 当前线程在数组中的引用存在,重新设置变量值
18         if (k == key) {
19             e.value = value;
20             return;
21         }
22         // Entry中持有的线程引用为空,从数组中删除此Entry对象
23         if (k == null) {
24             replaceStaleEntry(key, value, i);
25             return;
26         }
27     }
28     
29     // 将线程、变量值的映射设置到Entry对象中
30     tab[i] = new Entry(key, value);
31     // 数组中Entry对象的数量 + 1
32     int sz = ++size;
33     // 清除了数组中的某些元素 并且 当前Entry数组中的Entry对象的数量 大于等于 阀值
34     if (!cleanSomeSlots(i, sz) && sz >= threshold)
35         // 扩容处理
36         rehash();
37 }

  ThreadLocal采用线性探测的开放地址法去解决 hash 冲突。与HashMap的链地址法不同,ThreadLocal不会将hash冲突的数据放在链表上。当ThreadLocal 的 key 存在 hash 冲突,会线性地往后探测直到找到为 null 的位置存入对象,或者找到 key 相同的位置覆盖更新原来的对象。在这过程中若发现不为空但 key 为 null 的桶(key 过期的 Entry 数据)则启动探测式清理操作。

4.3、获取线程变量 - get源码分析

  根据ThreadLocal实例对象,获取当前线程的ThreadLocal.ThreadLocalMap中的Entry,若ThreadLocalMap不为空并且Entry不为空,返回Entry对象中的线程变量值Value;若Entry为空或者ThreadLocalMap为空,执行setInitialValue,初始化线程变量值,返回null。

  返回当前线程的线程变量值,ThreadLocal#get() 核心代码:

 1 ThreadLocal.ThreadLocalMap threadLocals = null;
 2 
 3 public T get() {
 4     // 获取当前线程
 5     Thread t = Thread.currentThread();
 6     // 获取当前Thread的 threadLocals 对象
 7     ThreadLocalMap map = getMap(t);
 8     // threadLocals不为null
 9     if (map != null) {
10         // ThreadLocal作为key,由于key获取数组下标,获取Entry对象
11         ThreadLocalMap.Entry e = map.getEntry(this);
12         // Entry不为null
13         if (e != null) {
14             // 返回ThreadLocal对应的value值
15             @SuppressWarnings("unchecked")
16             T result = (T)e.value;
17             return result;
18         }
19     }
20     // 可用来替换set()设置初始值,可实现自定义的初始化逻辑
21     return setInitialValue();
22 }
23 
24 // 设置初始值
25 private T setInitialValue() {
26     // ThreadLocal默认不实现,返回null,由子类实现
27     T value = initialValue();
28     // 获取当前线程
29     Thread t = Thread.currentThread();
30     // 获取当前Thread的 ThreadLocal.ThreadLocalMap 对象
31     ThreadLocalMap map = getMap(t);
32     // threadLocals不为null
33     if (map != null)
34         // 设置当前线程变量值
35         map.set(this, value);
36     else
37         // 创建Thread的 ThreadLocal.ThreadLocalMap 对象,存储ThreadLocal、value
38         createMap(t, value);
39     return value;
40 }
41 
42 // 初始化线程变量,ThreadLocal不实现,由子类实现
43 protected T initialValue() {
44     return null;
45 }
46 
47 // 获取Thread的  ThreadLocal.ThreadLocalMap 对象属性
48 ThreadLocalMap getMap(Thread t) {
49     return t.threadLocals;
50 }

  ThreadLocal的initialValue方法,默认返回null,可由具体子类实现。主要用于拓展,用户通过此方法可以将自定义内容设置到线程变量中。对于ThreadLocal而言,无论是调用set方法还是get方法,首次设置或获取线程变量,都会初始化当前线程的ThreadLocal.ThreadLocalMap。在ReentrantReadWriteLock中,对读锁的加锁与释放,应用了initialValue方法定义了持有线程重入的次数的对象HoldCounter。

  获取Entry对象,ThreadLocalMap#getEntry() 核心代码:

 1 // 获取Entry对象
 2 private Entry getEntry(ThreadLocal<?> key) {
 3     // ThreadLocal的hashCode 与 ThreadLocalMap中Entry数组长度 的 位运算获取数组下标
 4     int i = key.threadLocalHashCode & (table.length - 1);
 5     // 获取Entry对象
 6     Entry e = table[i];
 7     // Entry不为null,并且Entry中的referent 与 传入的ThreadLocal 引用相同
 8     if (e != null && e.get() == key)
 9         // 返回Entry对象
10         return e;
11     else
12         // 通过数组下标获取不到Entry对象的处理
13         return getEntryAfterMiss(key, i, e);
14 }
15 
16 // 通过ThreadLocal得到的数组下标,在Entry数组中无法获取到线程变量的处理
17 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
18     // 获取Entry数组
19     Entry[] tab = table;
20     // 获取数组长度
21     int len = tab.length;
22     // 线性探测地址法(Entry数组可能扩容,遍历指定下标的Entry)
23     while (e != null) {
24         // 获取ThreadLocal对象
25         ThreadLocal<?> k = e.get();
26         // Entry中的ThreadLocal与传入的ThreadLocal引用相同,返回线程变量值
27         if (k == key)
28             return e;
29         // Entry中的ThreadLocal为空,清理无效数据
30         if (k == null)
31             expungeStaleEntry(i);
32         else
33             // 数组下标 + 1
34             i = nextIndex(i, len);
35         // 重置Entry,循环终止条件
36         e = tab[i];
37     }
38     // 返回null
39     return null;
40 }
41 
42 // 清理Entry数组中的空闲对象,并重新计算Entry数组中的有效元素的数组下标,将有效Entry对象设置到指定下标
43 private int expungeStaleEntry(int staleSlot) {
44     // 获取Entry数组
45     Entry[] tab = table;
46     // 获取数组长度
47     int len = tab.length;
48 
49     // 清理空闲Entry对象
50     tab[staleSlot].value = null;
51     tab[staleSlot] = null;
52     // 数组的有效Entry对象减 1
53     size--;
54 
55     // 重新设置数组下标,循环处理,直到Entry数组中元素为null的为止
56     Entry e;
57     // 定义数组下标
58     int i;
59     // 遍历Entry数组
60     for (i = nextIndex(staleSlot, len);
61          (e = tab[i]) != null;
62          i = nextIndex(i, len)) {
63         // 获取Entry对象
64         ThreadLocal<?> k = e.get();
65         // Entry对象的refrent为null,即ThreadLocal引用为空
66         if (k == null) {
67             // 清除过期对象
68             e.value = null;
69             tab[i] = null;
70             size--;
71         // Entry对象的refrent不为null
72         } else {
73             // 重新设置数组中有效Entry的数组下标
74             int h = k.threadLocalHashCode & (len - 1);
75             // 重新获取的下标 与 原数组下标不一致,用重新获取的下标作为Entry的数组下标
76             if (h != i) {
77                 // 原数组下标的元素置为null
78                 tab[i] = null;
79 
80                 // 获取Entry数组中元素为null的下标
81                 while (tab[h] != null)
82                     h = nextIndex(h, len);
83                 // 设置Entry对象
84                 tab[h] = e;
85             }
86         }
87     }
88     return i;
89 }

  getEntry()方法在当前线程的ThreadLocal.ThreadLocalMap不为空时,通过ThreadLocal获取数组下标,从Entry数组中获取对应的Entry对象。若通过数组下标找不到Entry对象,遍历当前下标到数组尾部区间的数组元素,查找匹配的Entry对象,在此过程中会清除无效的Entry对象、重新计算Entry对象数组下标。

4.4、删除线程变量 - remove源码分析

  线程变量的删除,根据ThreadLocal获取数组下标,获取数组中的Entry对象,将Entry对象的引用属性refrent设置为null,同时执行expungeStaleEntry方法清除Entry数组中无效的元素。

  ThreadLocal的删除线程变量方法,最终执行ThreadLocalMap#remove(),核心代码:

 1 // 删除ThreadLocal
 2 private void remove(ThreadLocal<?> key) {
 3     // 获取当前Entry数组
 4     Entry[] tab = table;
 5     // 获取当前数组长度
 6     int len = tab.length;
 7     // 获取ThreadLocal对应的数组下标
 8     int i = key.threadLocalHashCode & (len-1);
 9     // 遍历当前数组下标到数组尾部区间的非null的Entry对象,遇到Entry为null的终止遍历
10     for (Entry e = tab[i];
11          e != null;
12          e = tab[i = nextIndex(i, len)]) {
13         // 匹配到指定的Entry对象
14         if (e.get() == key) {
15             // Entry对象的引用属性referent设置为null
16             e.clear();
17             // 清理Entry数组中无效的Entry对象
18             expungeStaleEntry(i);
19             return;
20         }
21     }
22 }

  Entry的clear方法,实际执行Reference#clear() 核心代码:

1 public void clear() {
2     // 将Entry对象的ThreadLocal对象的引用设置为null
3     this.referent = null;
4 }
  至此,ThreadLoal的源码分析结束。

五、有关ThreadLocal的面试题

  ThreadLocal保证的是原子性,每个线程都操作自己的数据,不会去操作临界资源。

5.1、ThreadLocal内存泄露

  ThreadLocal的内存泄露分为key的内存泄露、value的内存泄露。

1、内存泄露问题

  若ThreadLocal引用丢失,key是弱引用会被GC回收,如果对应value中的线程没有被回收,会导致内存泄露,内存中的value无法被回收,也无法被获取到。

2、内存泄露解决方案

  key是ThreadLocal本身,使用弱引用解决内存泄露问题,每次GC都会被回收,会将key置为null;

  0

  value内存泄露的解决方案,Entry[]数组中有remove方法,清理key为null的Entry对象。

  0