ThreadLocal原理
一、什么是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时,会回收弱引用对象占用的空间,防止内存泄露。

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 }
五、有关ThreadLocal的面试题
ThreadLocal保证的是原子性,每个线程都操作自己的数据,不会去操作临界资源。
5.1、ThreadLocal内存泄露
ThreadLocal的内存泄露分为key的内存泄露、value的内存泄露。
1、内存泄露问题
若ThreadLocal引用丢失,key是弱引用会被GC回收,如果对应value中的线程没有被回收,会导致内存泄露,内存中的value无法被回收,也无法被获取到。
2、内存泄露解决方案
key是ThreadLocal本身,使用弱引用解决内存泄露问题,每次GC都会被回收,会将key置为null;

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

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)