ThreadLocal 是一种用于实现线程局部变量的工具类。它允许每个线程都拥有自己的独立副本,从而实现线程隔离。
一、作用
每个线程访问的变量副本都是独立的,避免了共享变量引起的线程安全问题。由于 ThreadLocal 实现了变量的线程独占,使得变量不需要同步处理,因此能够避免资源竞争。ThreadLocal 可用于跨方法、跨类时传递上下文数据,不需要在方法间传递参数。
在 Web 应用中,可以使用 ThreadLocal 存储用户会话信息,这样每个线程在处理用户请求时都能方便地访问当前用户的会话信息。
在数据库操作中,可以使用 ThreadLocal 存储数据库连接对象,每个线程有自己独立的数据库连接,从而避免了多线程竞争同一数据库连接的问题。
在格式化操作中,例如日期格式化,可以使用 ThreadLocal 存储 SimpleDateFormat 实例,避免多线程共享同一实例导致的线程安全问题。
二、原理
创建一个 ThreadLocal 对象并调用 set 方法时,其实是在当前线程中初始化了一个 ThreadLocalMap。
ThreadLocalMap 是 ThreadLocal 的一个静态内部类,它内部维护了一个 Entry 数组,Entry 对象继承了 WeakReference,key 是 ThreadLocal 对象,value 是线程的局部变量,相当于为每个线程维护了一个变量副本,它并没有实现 Map 接口,是一个简单的线性探测哈希表。
Entry 继承了 WeakReference,它限定了 key 是一个弱引用,弱引用的好处是当内存不足时,JVM 会回收 ThreadLocal 对象,并且将其对应的 Entry.value 设置为 null,这样可以在很大程度上避免内存泄漏。
具体ThreadLocal 的set方法源码
set()
方法是 ThreadLocalMap 的核心方法,通过 key 的哈希码与数组长度取模,计算出 key 在数组中的位置,这一点和 HashMap 的实现类似。
get方法源码:
当调用 ThreadLocal.get()
时,会调用 ThreadLocalMap 的 getEntry()
方法,根据 key 的哈希码找到对应的线程局部变量。
解决hash冲突
如果计算得到的槽位 i 已经被占用,ThreadLocalMap 会采用开放地址法中的线性探测来寻找下一个空闲槽位:
如果 i 位置被占用,尝试 i+1。
如果 i+1 也被占用,继续探测 i+2,直到找到一个空位。
如果到达数组末尾,则回到数组头部,继续寻找空位。
为什么要用线性探测法而不是HashMap 的拉链法来解决哈希冲突?
ThreadLocalMap 设计的目的是存储线程私有数据,不会有大量的 Key,所以采用线性探测更节省空间。
拉链法还需要单独维护一个链表,甚至红黑树,不适合 ThreadLocal 这种场景
扩容机制
ThreadLocalMap 采用的是“先清理再扩容”的策略,扩容时,数组长度翻倍,并重新计算索引,如果发生哈希冲突,采用线性探测法来解决。
与 HashMap 不同,ThreadLocalMap 并不会直接在元素数量达到阈值时立即扩容,而是先清理被 GC 回收的 key,然后在填充率达到四分之三时进行扩容。
private void rehash() { // 清理被 GC 回收的 key expungeStaleEntries(); //扩容 if (size >= threshold - threshold / 4) resize(); }
清理过程会遍历整个数组,将 key 为 null 的 Entry 清除。
private void expungeStaleEntries() { Entry[] tab = table; int len = tab.length; for (int j = 0; j < len; j++) { Entry e = tab[j]; // 如果 key 为 null,清理 Entry if (e != null && e.get() == null) expungeStaleEntry(j); } }
阈值 threshold 的默认值是数组长度的三分之二。
private void setThreshold(int len) { threshold = len * 2 / 3; }
扩容时,会将数组长度翻倍,然后重新计算每个 Entry 的位置,采用线性探测法来寻找新的空位,然后将 Entry 放入新的数组中。
四种引用类型
1、强引用
被引用时不会被垃圾回收,内存空间不足会抛出outofmemory异常
new出一个对象
2、软引用
内存充足不会被回收,内存不足时会被回收。回收后内存还是不足会抛出内存不足异常
软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。
SoftReference的特点是它的一个实例保存对一个Java对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。
也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对 这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。
3、弱引用
垃圾回收时发现弱引用就直接回收
java.lang.ref.WeakReference
ThreadLocal和weakHashMap
4、虚引用
在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
用于nio
总结
ThreadLocal 的实现原理是,每个线程维护一个 Map,key 为 ThreadLocal 对象,value 为想要实现线程隔离的对象。
1、通过 ThreadLocal 的 set 方法将对象存入 Map 中。
2、通过 ThreadLocal 的 get 方法从 Map 中取出对象。
3、Map 的大小由 ThreadLocal 对象的多少决定
三、内存泄漏问题
原因
ThreadLocalMap 的 Key 是 弱引用,但 Value 是强引用。
如果一个线程一直在运行,并且 value 一直指向某个强引用对象,那么这个对象就不会被回收,从而导致内存泄漏。
怎么解决?
使用完 ThreadLocal 后,及时调用 remove()
方法释放内存空间。
try {
threadLocal.set(value);
// 执行业务操作
} finally {
threadLocal.remove(); // 确保能够执行清理
}
remove()
方法会将当前线程的 ThreadLocalMap 中的所有 key 为 null 的 Entry 全部清除,这样就能避免内存泄漏问题。
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 计算 key 的 hash 值
int i = key.threadLocalHashCode & (len-1);
// 遍历数组,找到 key 为 null 的 Entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 将 key 为 null 的 Entry 清除
e.clear();
expungeStaleEntry(i);
return;
}
}
}
public void clear() {
this.referent = null;
}
那为什么 key 要设计成弱引用?
弱引用的好处是,当内存不足的时候,JVM 能够及时回收掉弱引用的对象。
比如说:
WeakReference key = new WeakReference(new ThreadLocal());
key 是弱引用,new WeakReference(new ThreadLocal())
是弱引用对象,当 JVM 进行垃圾回收时,只要发现了弱引用对象,就会将其回收。
一旦 key 被回收,ThreadLocalMap 在进行get()
、set()
的时候就会对 key 为 null 的 Entry 进行清理。
四、改进方案
在 JDK 20 Early-Access Build 28 版本中,出现了 ThreadLocal 的改进方案,即
ScopedValue
。还有 Netty 中的 FastThreadLocal,它是 Netty 对 ThreadLocal 的优化,内部维护了一个索引常量 index,每次创建 FastThreadLocal 中都会自动+1,用来取代 hash 冲突带来的损耗,用空间换时间。
以及阿里的 TransmittableThreadLocal,不仅实现了子线程可以继承父线程 ThreadLocal 的功能,并且还可以跨线程池传递值。
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// 在父线程中设置
context.set("value-set-in-parent");
// 在子线程中可以读取,值是"value-set-in-parent"
String value = context.get();
评论区