当前位置: 首页 > news >正文

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 类的 setget方法时才创建

为什么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();
    }
}
  1. 使用静态变量存放 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();
    }
}

相关文章:

  • c++中,什么时候应该使用mutable关键字?
  • Bash Shell控制台终端命令合集
  • C语言番外篇(3)------------>break、continue
  • 论文笔记:Autonomy-of-Experts Model
  • watchEffect 里有响应式依赖时并没有自动追踪
  • C++关键字之mutable
  • Tesseract OCR:起源、发展与完整使用指南
  • 多线程篇学习面试
  • 请谈谈 Vue 中的 key 属性的重要性,如何确保列表项的唯一标识?
  • 设计模式Python版 中介者模式
  • Vue 3 + Vite 项目中配置代理解决开发环境中跨域请求问题
  • Linux系统管理与编程01:准备工作
  • vim 多个关键字高亮插件介绍
  • A. Jagged Swaps
  • mybatis从接口直接跳到xml的插件
  • 不同activity的mViewModel是复用同一个的还是每个activity都是创建新的ViewModel
  • DeepSeek各模型现有版本对比分析
  • Python selenium 库
  • 轻松将 Python 应用移植到 Android,p4a 帮你实现
  • 485. 最大连续 1 的个数
  • “子宫肌瘤男性病例”论文后:“宫颈癌、高危产妇”论文也现男性病例,作者称“打错了”
  • 央广网评政府食堂打开大门:小城文旅爆火的底层密码就是真诚
  • 传奇落幕!波波维奇卸任马刺队主教练,转型全职球队总裁
  • 人民日报评论员:把造福人民作为根本价值取向
  • 美法官裁定特朗普援引战时法律驱逐黑帮违法,系首次永久性驳回
  • “非思”的思想——探索失语者的思想史