Java并发 ThreadLocal 原理(详解)
ThreadLocal是什么
在并发编程中,多个线程可能会同时访问和修改共享变量,导致线程安全问题。 而ThreadLocal
允许每个线程都有自己的独立变量副本,避免了多线程间的变量共享和竞争,从而解决了线程安全问题。
与通过加锁、同步块等传统方式来保证线程安全相比。ThreadLocal
不需要对变量访问进行同步,减少了上下文切换、锁竞争的性能损耗。
原理
ThreadLocal
实现资源隔离的核心思想就是
在每个线程Thread
类中会有一个独立的变量副本ThreadLocal
字段,它内部维护一个ThreadLocalMap
,用于存储线程独立的变量副本
-
当调用
ThreadLocal.set
方法时,会将当前ThreadLocal
对象和要添加的值放入当前线程的ThreadLocalMap
中。 -
当调用
ThreadLocal.get
方法时,会从 当前线程的ThreadLocalMap
中查找 这个ThreadLocal
对象也就是key对应的Value
值。
不同线程通过 ThreadLocal
获取各自内部的变量副本,而不会影响其他线程的数据。
源码分析
具体就是分析Thread
线程类的源码,可以发现它里面会有个ThreadLocal.ThreadLocalMap
类型的变量,用来保存本地变量。
而ThreadLocalMap
是ThreadLoal里面的静态内部类
public class Thread implements Runnable { //...... //与此线程有关的ThreadLocal值。由ThreadLocal类维护 ThreadLocal.ThreadLocalMap threadLocals = null; //...... }
默认情况下这个变量是 null,只有当前线程调用 ThreadLocal
类的 set
或get
方法时才创建
为什么
ThreadLocalMap
放在 Thread 里面使用,而且还 定义成 ThreadLocal 的静态内部类呢?
因为内部类这个东西是编译层面的概念,就像语法糖一样,经过编译器之后其实内部类会提升为外部顶级类,和平日里外部定义的类没有区别,也就是说在JVM
中是没有内部类这个概念的。
而静态外部类其实就等于一个顶级类,可以独立于外部类使用,把这个Map放在ThreadLocal
用意就是说明 ThreadLocalMap
是和 ThreadLocal
强相关的,专用于保存线程本地变量。更多的只是表明类结构和命名空间
现在我们来看一下 ThreadLocalMap 的定义:
首先ThreadLocalMap
里面有个Entry
数组,键是ThreadLocal
对象,value值是我们需要保存的值,和HashMap 有点类似
这个 Entry 继承了 WeakReference
弱引用。具体弱引用的是entry内部这个ThreadLocal
键 ---看到Entry 构造函数的super(k)
没
get流程
比如要调用threadLocal对象的get方法
首先它会获取当前线程,得到这个线程里面的ThreadLocalMap
变量,然后将当前这个threadLocal
对象作为key去当前线程中的 ThreadLocalMap
找对应的 Entry ,至于如何查找的,其实也很简单
每个threadLocal
对象作为键都会 计算得到一个hash值,然后和hashMap一样,计算得到entry数组内的一个下标,去查找这个threadLocal对象
但是对于处理Hash冲突,HashMap
是通过链表(红黑树)法来解决冲突,而 ThreadLocalMap 是通过开放寻址法来解决冲突。
如果通过 key 的哈希值得到的下标无法直接命中,则会将下标 +1,即继续往后遍历数组查找 Entry 直到找到或者返回 null。
虽然用开放寻址法效率不高,但是ThreadLoacl它的对象键其实也不是很多,所以就用这种简单的方式
set流程
再比如要调用threadLocal对象的set方法,为当前线程设置变量副本的值,也是同理
内部逻辑是:先通过threadLocal
这个对象键的 hash 值计算出一个数组下标,然后看看这个下标是否被占用了
-
如果被占了看看是否就是要找的 Entry,如果是则进行更新
-
如果不是则下标++,即往后遍历数组,查找下一个位置,找到空位就 new 个 Entry 然后把坑给占用了。就是前面所说的开放地址法
当然,这种数组操作一般免不了阈值的判断,如果超过阈值则需要进行扩容。
总的来说和HashMap底层原理差不多
举例
比如现在有3个 ThreadLocal 对象,2 个线程。
ThreadLocal<T>(T initialValue):创建一个带有初始值的 ThreadLocal 实例
ThreadLocal.withInitial(Supplier<T> supplier):创建一个 ThreadLocal 实例,并使用 supplier 提供的值作为初始值。
remove():移除当前线程的变量副本,释放资源,避免内存泄漏
// 创建三个 ThreadLocal 对象
ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();
// 创建两个线程
new Thread(() -> {
threadLocal1.set("Thread 1 Value"); //也可以使用withInitial,可以初始化默认值
threadLocal2.set(1);
threadLocal3.set(2);
System.out.println("Thread 1 values: " + threadLocal1.get() + ", " + threadLocal2.get() + ", " + threadLocal3.get());
}).start();
new Thread(() -> {
threadLocal1.set("Thread 2 Value");
threadLocal2.set(3);
threadLocal3.set(4);
System.out.println("Thread 2 values: " + threadLocal1.get() + ", " + threadLocal2.get() + ", " + threadLocal3.get());
}).start();
//两个线程 并发输出
Thread 1 values: Thread 1 Value, 1, 2
Thread 2 values: Thread 2 Value, 3, 4
使用 ThreadLocal 时需要用弱引用来防止内存泄漏?
使用弱引用作为ThreadLocal
的键可以防止内存泄漏。
若 ThreadLocal
实例被不再需要的线程持有为强引用,那么当该线程结束时,相关的 ThreadLocal实例可能无法被回收,导致内存持续占用。而弱引用允许垃圾回收器在内存不足时回收对象。
为什么要这样设计呢?
因为 线程在我们应用中,常常是以线程池的方式来使用的,比如 Tomcat 的线程池处理了一堆请求,而线程池中的线程一般是不会被清理掉的,所以这个引用链就会一直在,那么 ThreadLocal 对象即使没有用了,也会随着线程的存在,而一直存在着
所以这条引用链需要弱化一下,能操作的只有 Entry 和 key 之间的引用,所以它们之间用弱引用来实现。
内存泄漏
内存泄漏就是 程序中已经无用的内存无法被释放,造成系统内存的浪费。
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
在每个线程的ThreadLocalMap
中
-
key 是弱引用:
ThreadLocalMap
中的 key 是ThreadLocal
的弱引用。 这意味着,如果ThreadLocal
实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致ThreadLocalMap
中对应的 key 变为null
。 -
而value 是强引用:
ThreadLocalMap
中的 value 是强引用。 即使 key 被回收(变为null
),value 仍然存在于ThreadLocalMap
中
所以当 ThreadLocal
实例失去强引用后,key 变为 null
。但其对应的 value 仍然存在于 ThreadLocalMap
中,所以导致导致 key 为 null
的 entry 无法被垃圾回收,造成内存泄漏。
-
此外 程序员本身也可以在使用完
ThreadLocal
后,调用remove()
方法。从ThreadLocalMap
中显式地移除对应的 entry,彻底解决内存泄漏的风险。
如果将 value 也设置为弱引用,是否可以防止内存泄漏?
答案肯定是可以的。但是一次 gc 就没了,等用到的时候不就找不到 value 了 ,所以 value 不能被设置为弱引用
那既然会有内存泄漏还这样实现?
为了避免内存泄漏,设计者在多个地方都做了清理无用 Entny,即回收key为null
的 Entry
-
比如通过 key 查找 Entry 的时候,如果下标无法直接命中,那么就会向后遍历数组,此时遇到key为 null 的Entry 就会清理掉
-
还有像扩容的时候也会清理无用的 Entry
使用 ThreadLocal 的最佳实践
因为可能会出现内存泄露问题,所以,最佳实践是用完了之后,调用一下remove 方法,手动把 Entry 清理掉,这样就不会发生内存泄漏了
void yesDosth { threadlocal.set(xxx); try { // do sth } finally { threadlocal.remove(); } }
-
使用静态变量存放
ThreadLocal
将Threadlocal
作为类的静态变量保存,这样可以确保同一个线程的局部变量在线程的生命周期内都可以被访问,避免对象频繁创建。
public class ThreadLocalExample { // 静态 ThreadLocal 存储每个线程独立的副本 private static final ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Default Value"); public static void main(String[] args) { Runnable task1 = () -> { System.out.println(Thread.currentThread().getName() + " Initial: " + threadLocal.get()); threadLocal.set("Task 1 Value"); System.out.println(Thread.currentThread().getName() + " Modified: " + threadLocal.get()); threadLocal.remove(); // 移除值,避免内存泄漏 }; Runnable task2 = () -> { System.out.println(Thread.currentThread().getName() + " Initial: " + threadLocal.get()); threadLocal.set("Task 2 Value"); System.out.println(Thread.currentThread().getName() + " Modified: " + threadLocal.get()); threadLocal.remove(); // 移除值,避免内存泄漏 }; new Thread(task1).start(); new Thread(task2).start(); } }