ThreadLocal的应用

1. ThreadLocal 是什么

JDK 对ThreadLocal的描述为:

此类提供线程局部变量。这些变量与普通变量的不同之处在于,每个访问一个变量的线程(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段,这些字段希望将状态与线程(例如,用户ID或事务ID)相关联。

说白了,ThreadLocal就是用来存放线程自身相关数据的一个容器,这个容器叫做ThreadLocalMap,它是ThreadLocal的一个静态内部类,同时作为Thread类的一个成员变量。ThreadLocal在使用时,先拿到当前线程的成员变量ThreadLocalMap,以当前的ThreadLocal对象作为key,变量作为value存入ThreadLocalMap。 然后每个线程取变量都是从线程各自的ThreadLocalMap中取值,自然是线程安全的了。因为变量只在自己线程的生命周期内起作用,所以说ThreadLocal提供线程局部变量,或者叫线程本地变量。

ThreadLocal 的特点有3个:

  1. 线程并发:在多线程并发的场景下使用。
  2. 数据传递:通过 ThreadLocal ,在同一个线程中,不同组件中传递公共变量。
  3. 线程隔离:不同线程之间互不干扰,这种变量在线程的生命周期内起作用。

2. ThreadLocal 怎么用

ThreadLocal 的常用方法有:

  1. public ThreadLocal():通过构造器创建对象。一般是静态的。
  2. <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier):初始化一个 ThreadLcoal。
  3. void set(T value):设置当前线程绑定的局部变量。
  4. T get():获取当前线程绑定的局部变量。
  5. void remove():删除当前线程绑定的局部变量。

2.1 使用入门

2.1.1 原始版本

现在模拟一个需求,一个线程在业务开始时初始化一个用户 id(类似在一次web请求中上下文中初始化一下用户信息),业务结束时获取这个用户 id(比如用来打印日志,或者作为一个公共变量运用到业务编码中),存在多个这样的线程。

public class ThreadLocalTest {
    private String userId;

    private String getUserId() {
        return userId;
    }

    private void setUserId(String userId) {
        this.userId = userId;
    }

    public static void main(String[] args) {
        ThreadLocalTest test = new ThreadLocalTest();
        for (int i = 1; i < 6; i++) {
            Thread thread = new Thread(() -> {
                // 当前线程初始化userId
                test.setUserId(Thread.currentThread().getName() + "的userId");
                // 执行其他业务代码
                System.out.println("===执行业务代码===");
                // 当前线程获取userId
                System.out.println(Thread.currentThread().getName() + "-->" + test.getUserId());
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

一种可能的结果:

===执行业务代码===
线程2-->线程1的userId
===执行业务代码===
线程1-->线程3的userId
===执行业务代码===
线程3-->线程3的userId
===执行业务代码===
线程4-->线程4的userId

由于线程调度的不确定性,可能线程1运行到一半,切换到了线程2,于是线程2获取到的 userId 是线程1设置的。也就是说,每个线程之间的变量不是隔离的,造成数据错误。

2.1.2 ThreadLocal 版本

每个线程中的变量都存放到自己的线程当中,所以这些变量叫做线程局部变量很形象。

public class ThreadLocalTest {
    private static ThreadLocal<String> context = new ThreadLocal<>();

    private String getUserId() {
        return context.get();
    }

    private void setUserId(String userId) {
        context.set(userId);
    }

    public static void main(String[] args) {
        ThreadLocalTest test = new ThreadLocalTest();
        for (int i = 1; i < 5; i++) {
            Thread thread = new Thread(() -> {
                test.setUserId(Thread.currentThread().getName() + "的userId");
                System.out.println("===执行业务代码===");
                System.out.println(Thread.currentThread().getName() + "-->" + test.getUserId());
                context.remove(); // 使用完清理线程局部变量
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

这样每个线程就互不干扰,不会取错变量值。一种可能的结果如下:

===执行业务代码===
线程1-->线程1的userId
===执行业务代码===
线程4-->线程4的userId
===执行业务代码===
线程2-->线程2的userId
===执行业务代码===
线程3-->线程3的userId

2.1.3 synchronized 版本

如果只看结果的正确性,用 synchronized 给业务代码块加锁也是可以完成的。如下:

Thread thread = new Thread(() -> {
    synchronized (ThreadLocalTest.class) {
        test.setUserId(Thread.currentThread().getName() + "的userId");
        System.out.println("===执行业务代码===");
        System.out.println(Thread.currentThread().getName() + "->" + test.getUserId());
    }
});

这样完全可以实现需求,但是 synchronized 的问题是什么呢?我们总说谁谁谁是线程安全的类,因为它有 synchronized 修饰。就是因为 synchronized 让多线程变成了单线程,它一次只允许一个线程执行,它能不安全吗?但它带来的代价是性能的下降,它不能并发执行,而 ThreadLocal 可以并发执行。

2.1.4 ThreadLocal 和 synchronized 对比

综上,synchronized 和 ThreadLocal 两个处理问题的角度和场景是不同的。

  • synchronized 的侧重点在于保证操作的原子性,保证并发场景下共享变量的数据一致性。
  • ThreadLocal 强调线程隔离性,不同的线程互不干扰,保证并发场景下数据传递的正确性。在web请求上下文中较为常见。

3. ThreadLocal 原理

3.1 代码结构

ThreadLocal 的原理要从它的set(T value)get()方法的源码入手。在 set 值的时候,首先会获取当前线程一个的成员变量ThreadLocalMapThreadLocalMap的 key 是当前ThreadLocal对象,value 是要存入的值。这个 key 和 value 会存到哪里呢?ThreadLocalMap还有个内部类Entry,这个Entry继承了WeakReference,key 赋值给弱引用,也就是当前的ThreadLocal对象,value 则赋值给Entry的成员变量valueThreadLocalMap也是一个哈希表(所谓哈希表,也叫散列表,它基于数组,通过某种哈希算法计算出一系列关键字对应的散列值,然后以这些散列值作为数组索引将数据存放到对应位置,达到快速查找的目的),它内部维护一个Entry数组,来存储键值对。存数据的时候也是通过哈希函数计算ThreadLocal 对象对应的数组下标,然后放入Entry数组中。

3.2 内存泄漏问题

ThreadLocal 会发生内存泄漏吗?我们结合代码慢慢分析。

在 2.1.1 节中有这样的代码:

public class ThreadLocalTest {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    private void setUserId(String userId) {
        threadLocal.set(userId);
    }
    // ...
}

首先,我们new了一个 ThreadLocal 对象,这里存在一个强引用:threadLocal引用变量指向 ThreadLocal 对象。其次,当其他线程执行setUserId方法时,ThreadLocal 的set方法最终是把数据存到了ThreadLocalMap中的Entry,看源码我们会发现,存数据最终是调用Entry的构造器Entry(ThreadLocal<?> k, Object v)完成的,而k这个参数是传入的this对象,说明什么?我们使用 ThreadLocal 对象调用set,那this肯定是当前new出来的 ThreadLocal 对象!再次说明,我们new出来的 ThreadLocal 对象有两个引用指向它:

  1. threadLocal变量的强引用。
  2. Entry中的弱引用。

此时再看一张图(这张图被广泛引用,感谢原图作者😂):

  1. 堆内存里面有个 ThreadLocal 对象,它被两个箭头指着,实线代表强引用,虚线代表弱引用。
  2. 有两个引用链,一个是我们手动创建的threadLocal的引用变量指向的,即图中的 ThreadLcoal Ref 对应示例代码中的threadLocal变量;一个是由于调用了 ThreadLocal 的setget方法,初始化了当前线程的ThreadLocalMap,再初始化 Map 中的Entry对象,再初始化Entry对象中的 key 和 value,形成一个由当前线程对象到它内部变量的引用链,即上图中的 Current Thread Ref,它对应set方法源码中的这一行Thread t = Thread.currentThread();中的变量t

那问题来了,如果这个手动创建的 ThreadLocal 对象 的『引用变量』被回收了,那 ThreadLocal 对象 是不是只剩下Entry中 key 的弱引用了?而弱引用的对象会随时被 GC 回收,即Entry中的 key 会在 GC 后变为null了。我们知道,ThreadLocalMap的 key 是当前的 ThreadLocal 对象,那 key 为null了之后,就无法获取到Entry,也取不到 value 的值了。在Entry对象没有被主动删除,或者当前线程没有终结的情况下,该Entry一直处在一个由当前线程指向的强引用链中。由于这个Entry获取不到,就一直占用着内存,又因为强引用不能被 GC 回收,所以这个Entry就发生了内存泄漏。如果这个线程是一个普通线程,在线程终止的时候,整个线程对象被回收了,那内存泄漏的时间比较短;如果该线程一直不终止,比如线程池中的核心线程,那内存泄露问题就一直存在了。

注意,上面说的“如果这个手动创建的 ThreadLocal 对象 的『引用变量』被回收了”,应该会有人疑惑这种情况什么时候会发生呢?第一种情况,手动把这个引用变量置为null,虽然概率小,但也不是没可能;第二种情况,引用变量是存在栈内存中,当方法执行完,就会立即回收栈内存中的引用变量,即堆内存中的实际对象失去引用指针了。这种情况就比如 ThreadLocal 是在方法中创建的局部变量。

3.3 为什么使用弱引用

Entry的 key 使用弱引用有内存泄漏风险,那为什么 JDK 还是使用弱引用而不是强引用?

我们分两种情况讨论:

  • key 使用强引用:ThreadLocal 的引用变量被回收了,这句话意味着什么呢?引用变量被回收了,意味着代码中不再使用 ThreadLocal 这个对象了,因为要使用 ThreadLocal 这个对象,我们需要用它的引用变量取调setget方法,现在引用变量没了,我们就用不了 ThreadLocal 这个对象了。但问题是,ThreadLocalMap还持有ThreadLocal对象的强引用,当前线程到Entry的强引用链依然存在。注意,前面提到了,ThreadLocal 对象已经不再使用了,也就是说Entry就获取不到了。如果Entry没有手动删除,或者线程没有结束,这个没用的Entry也会一直保留,依然发生内存泄漏(要明白内存泄漏是对象没用了,还存在内存中不被回收的情况)。
  • key 使用弱引用:前面已经分析过了,ThreadLocal 的引用变量被回收了,ThreadLocal对象也被回收,导致Entry的 key 变成null,在没有手动删除Entry或线程不结束时依然发生内存泄漏。

归根结底,由于ThreadLocalMap的生命周期跟Thread一样长,在 ThreadLocal 的引用变量消失后,如果线程不结束,原来的Entry就不会回收,这就是内存泄漏的本质。虽然 ThreadLocal 在每次读写数据的时候,都会将keynullEntry清空,但是,既然 ThreadLocal 的引用变量都消失了,我们也没机会再setget了。

那为什么使用弱引用?我也不知道!我还没想明白,如果正在阅读的你知道,请你告诉我下,谢谢😅。虽然ThreadLocalMap的注释中解释了:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.

为了帮助处理非常大和长期的使用,哈希表条目使用WeakReferences作为键。

我觉得没必要取纠结这个问题,只要规范的使用 ThreadLocal,几乎不会发生内存泄漏。

3.4 如何防止内存泄漏

  1. 把 ThreadLocal 对象申明为类变量。类变量的生命周期跟 JVM 是同步的,这样 ThreadLocal 的强引用就一直存在,不会被 GC 回收,Entrykey就不会发生null的情况了。
  2. 使用完 ThreadLocal 后,用remove()方法,清空当前ThreadLocal 对应的数据,对应的Entry就不占内存了。

第一种情况虽热能避免Entry的key为null的情况,但是如果后续线程不再访问这个 key,且线程不结束时,这个 key 对应的数据也会一直存在内存中,容易造成内存溢出的问题。所以最好的办法就是在 ThreadLocal 使用完之后,使用remove()方法清除数据。

4. ThreadLocal 如何存多个变量

上面的示例代码中,ThreadLocal 只存了一个变量,实际情况不可能只存一个吧,多个变量如何存,如何取?

要知道 ThreadLocal 使用set方法存数据时,key 用的this对象,就是当前正在使用的 ThreadLocal 对象,说明一个 ThreadLocal 对象,在一个线程中,只能存一个线程本地变量。多个线程虽然都是用的是一个 key,但是不同的线程用的是不同的ThreadLocalMap

第一种方案是多 new 几个 ThreadLocal 对象,每个 ThreadLocal 对象对应一个业务变量。

第二种方法就是在给 ThreadLocal 初始化一个HashMap,这是最常规的做法。比如下面:

public class ThreadLocalTest {
    private static final ThreadLocal<Map<String, Object>> context =
            ThreadLocal.withInitial(HashMap::new);

    private String getUserId() {
        return String.valueOf(context.get().get("userId"));
    }

    private void setUserId(String userId) {
        context.get().put("userId", userId);
    }

    public void setUserName(String userName) {
        context.get().put("userName", userName);
    }

    public String getUserName() {
        return String.valueOf(context.get().get("userName"));
    }

    public static void main(String[] args) {
        ThreadLocalTest test = new ThreadLocalTest();
        for (int i = 1; i < 5; i++) {
            Thread thread = new Thread(() -> {
                String threadName = Thread.currentThread().getName();
                test.setUserId(threadName + "的userId");
                test.setUserName(threadName + "的userName");
                System.out.println("===执行业务代码===");
                System.out.println(threadName + "-->" + test.getUserId() + "," + test.getUserName());
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

一种可能的结果:

===执行业务代码===
线程2-->线程2的userId,线程2的userName
===执行业务代码===
线程4-->线程4的userId,线程4的userName
===执行业务代码===
线程3-->线程3的userId,线程3的userName
===执行业务代码===
线程1-->线程1的userId,线程1的userName

5. 为什么用 ThreadLocal

5.1 ThreadLocal的使用场景

线程的上下文传递。企业中最常见的是应用到web请求的上下文,一个 Http 请求会经过一系列拦截器,过滤器最后到达服务层,在这个调用链路中,会频繁的使用到一些公共数据,如用户信息或请求的ID,把这些公共数据放到 ThreadLocal 中,会在请求的链路中非常方便的使用这些信息。

还有一些框架中会使用 ThreadLocal 来管理数据库连接,避免了线程之间的竞争。比如 Mybatis 就是用 ThreadLocal 来存储Sqlsession对象。

5.2 使用 ThreadLocal 的好处

使用 ThreadLocal 的好处是并发场景下减少了同一个线程内多个函数或组件之间传递公共变量的复杂度,且提高了使用这些共享变量的安全性。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/24035.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Centos7安装Java8(在线安装避坑详细安装)

开篇语&#xff1a; 喜欢在一个明媚阳光的午后 坐在那夕阳斑驳的南墙下 听着风起 闻着花香 望着远山 身边是你 如此便觉得很好 1.查看目前环境 rpm -qa|grep jdk在这里我们会发现&#xff0c;原有系统安装有jdk&#xff0c;如果对于jdk有要求&#xff0c;我们就需要重新安装jdk…

面了一个测试工程师要求月薪26K,总感觉他背了很多面试题...

最近有朋友去字节面试&#xff0c;面试前后进行了20天左右&#xff0c;包含4轮电话面试、1轮笔试、1轮主管视频面试、1轮hr视频面试。 据他所说&#xff0c;80%的人都会栽在第一轮面试&#xff0c;要不是他面试前做足准备&#xff0c;估计都坚持不完后面几轮面试。 其实&…

3DMAX车缝线生成器插件使用方法详解

3dMax车缝线生成器插件,用于创建缝合对象和一个对象,以沿样条线或仅通过绘制选定边上的缝合之间的孔。 目前有两种类型的缝线,圆形缝线和平面缝线。对于给定类型的针脚,它们的厚度是最常用的。缝线的长度和间距以及旋转都可以很容易地调整,这些参数也可以随机设置,以创造…

[C语言][典例详解]打印杨辉三角(找规律简单实现)

目录 杨辉三角的相关知识 杨辉三角图&#xff1a; 杨辉三角的规律 在编程中实现 第一步 &#xff1a;我们先实现数字的打印&#xff0c;后面再加上空格构成三角形形状&#xff1b; ​编辑 1.首先我们可以直观的看出三角形的两个斜边都是1&#xff1b;所以我们先打印斜边的…

Python自动化测试框架有哪些?怎么选

目录 自动化测试框架概念 自动化测试框架根据思想理念和深度不同&#xff0c;渐进式的分为以下几种&#xff1a; 模块化测试脚本框架&#xff1a; 测试库框架&#xff1a; 数据驱动测试框架&#xff1a; 关键字驱动或表驱动的测试框架&#xff1a; 混合测试自动化框架&am…

沉浸式翻译 安装及使用

介绍一下最近非常或的沉浸式翻译工具&#xff0c;非常有助于外文阅读&#xff0c;包括网页、pdf等。可以同时显示原文和译文&#xff0c;操作简单&#xff0c;使用起来还是非常友好的。 先上链接&#xff1a;介绍 - 沉浸式翻译 如何使用 - 沉浸式翻译 1.安装 支持Edg…

Linux——使用命令行参数管理环境变量

目录 使用命令行参数获取用户在DOS命令行输入的指令&#xff1a; 方法&#xff1a;代码如下&#xff1a; 使用命令行参数获取并打印部分或者整体环境变量的方法&#xff1a; 方法1&#xff1a; 运行结果&#xff1a; 方法2&#xff1a;使用外部链接environ: 使用命令行参数…

article-并联机械手爪运动学分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3aNKIR4E-1685371700448)(data:image/svgxml;utf8, )] 2.4.3 基于Robotics Toolbox的工具箱的模型检测 上文中&#xff0c;我们已经对采摘机器手爪运动学理论模型进行了创建&#xff0c;接下来要用MA…

【智慧排水】智慧排水监测系统助力城市抗洪排涝建设

随着城市的发展和生活水平的提高&#xff0c;城市排水系统面临着各种挑战和难题。虽然国家已经大力建设和改造雨污分流系统&#xff0c;以解决城市排水问题&#xff0c;但在实际应用中仍然存在着诸多难题&#xff0c;如雨污混接、偷排漏排、管道堵塞淤积、管道溢流和内涝等问题…

没有经验能做产品经理吗?

没有经验能做产品经理吗&#xff1f;这是一个经常被讨论的问题&#xff0c;因为很多人想转行成为产品经理&#xff0c;但他们没有相关的工作经验。这里我也给出一些解答。 一、产品经理的职责和技能 首先&#xff0c;让我们看一下产品经理的职责和技能。产品经理是负责产品开…

java项目打包方式

普通项目打包 项目内容很简单&#xff0c;只是引用了一个三方包。 打包步骤 File-Project Structure... 点击确定后选择Build - Build Artifacts.. 选择build即可&#xff0c;可以查看编译日志 maven项目打包 若果是普通项目就先转为maven项目。 右键项目选择第二项add frame…

SpringCloud Nacos实战应用

目录 1 Nacos安装1.1 Nacos概要1.2 Nacos架构1.3 Nacos安装1.3.1 Nacos Derby安装1.3.2 Nacos MySQL版安装1.3.3 Docker 安装Nacos 2 Nacos功能应用2.1 Nacos服务注册与发现2.2 负载均衡2.3 配置中心2.4 灰度发布 3 Nacos集群3.1 集群架构3.2 Nacos集群部署3.3 客户端接入Nacos…

华为OD机试之打印机队列(Java源码)

打印机队列 题目描述 有5台打印机打印文件&#xff0c;每台打印机有自己的待打印队列。 因为打印的文件内容有轻重缓急之分&#xff0c;所以队列中的文件有1~10不同的代先级&#xff0c;其中 数字越大优先级越高 打印机会从自己的待打印队列中选择优先级最高的文件来打印。 如…

Windows 上安装和启动 Nacos 2.2.2 最新版本

文章目录 前言版本声明本地启动1. 下载 Nacos2. 开启鉴权配置3. 持久化数据库4. 启动 Nacos5. 启动测试 联系我 前言 本文旨在为您详细介绍如何安装和启动 Nacos 2.2.2 的最新版本&#xff0c;以及为 youlai-mall 开源商城版本的升级做好准备工作。 版本声明 名称版本操作系…

3年外包裸辞,面试阿里、字节全都一面挂,哭死.....

测试员可以先在外包积累经验&#xff0c;以后去大厂就很容易&#xff0c;基本不会被卡&#xff0c;事实果真如此吗&#xff1f;但是在我身上却是给了我很大一巴掌... 所谓今年今天履历只是不卡简历而已&#xff0c;如果面试答得稀烂&#xff0c;人家根本不会要你。况且要不是大…

c#快速入门

欢迎来到Cefler的博客&#x1f601; &#x1f54c;博客主页&#xff1a;那个传说中的man的主页 &#x1f3e0;个人专栏&#xff1a;题目解析 &#x1f30e;推荐文章&#xff1a;题目大解析2 目录 &#x1f449;&#x1f3fb; c#和c不同之处&#x1f449;&#x1f3fb;程序文件的…

已签名驱动程序安装后提示“Windows无法验证此设备所需驱动程序数字签名”的原因和解决方法

在Windows 64位系统上&#xff0c;正常开启数字签名认证时&#xff0c;驱动程序软件需要经过微软数字签名的才允许被使用。否则在设备管理器下&#xff0c;安装完硬件驱动后设备上会有“黄色感叹号”标识&#xff0c;右键该设备属性提示&#xff1a;“Windows 无法验证此设备所…

SY8205同步降压DCDC可调电源模块(原理图和PCB)

SY8205同步buck降压电源模块&#xff0c;输入电压4.5-30V&#xff0c;输出电压0.6-30V可调&#xff0c;效率90%以上&#xff0c;最大连续输出电流5A&#xff0c;峰值电流6A。 开源链接&#xff1a;https://url.zeruns.tech/obGu3 SY8025数据手册下载地址&#xff1a;https://…

公文写作素材:为人处世类过渡句50例

1.身处逆境&#xff0c;敢于亮剑&#xff0c;坚毅前行&#xff0c;方能逆势突围&#xff1b;面对困难&#xff0c;坚定信心&#xff0c;敢拼敢闯&#xff0c;定能笑到最后。 2.没有海纳百川的胸怀&#xff0c;怎能容得下不同性格的人&#xff1b;没有从善如流的雅量&#xff0…

java程序1补充:从键盘输入圆的半径,求圆的周长和面积(简易与交互两版)

编写一个java程序&#xff0c;从键盘输入圆的半径&#xff0c;求圆的周长和面积&#xff0c;并输出。 要求&#xff1a; &#xff08;1&#xff09;半径仅考虑int型正整数&#xff0c;并综合利用所学较好地处理异常输入&#xff0c;包括非法整数、负整数输入时的处理。输入半…