JAVA高并发——人手一支笔:ThreadLocal

news2025/1/17 21:40:28

文章目录

  • 1、ThreadLocal的简单使用
  • 2、ThreadLocal的实现原理
  • 3、对性能有何帮助
  • 4、线程私有的随机数发生器ThreadLocalRandom
    • 4.1、反射的高效替代方案
    • 4.2、随机数种子
    • 4.3、探针Probe的作用

除了控制资源的访问,我们还可以通过增加资源来保证所有对象的线程安全。比如,让100个人填写个人信息表,如果只有一支笔,那么大家就得挨个填写,对于管理人员来说,必须保证大家不会去哄抢这仅有的一支笔,否则,谁也填不完。从另一个角度出发,我们可以准备100支笔,人手一支,那么所有人很快就能完成表格的填写工作。

如果说锁使用的是第一种思路,那么ThreadLocal使用的就是第二种思路。

1、ThreadLocal的简单使用

从ThreadLocal这一名字上可以看出,这是一个线程的局部变量。也就是说,只有当前线程可以访问。既然是只有当前线程可以访问的数据,那么自然是线程安全的。

下面来看一个简单的示例:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @title ThreadLocalDemo
 * @description ThreadLocal测试
 * @author: yangyongbing
 * @date: 2024/2/20 12:22
 */
public class ThreadLocalDemo implements Runnable{
    private  static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    int i=0;

    public ThreadLocalDemo(int i) {
        this.i = i;
    }

    @Override
    public void run() {
        try {
            Date t = sdf.parse("2024-02-20 19:29:" + i % 60);
            System.out.println(i+":"+t);
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new ThreadLocalDemo(i));
        }
    }
}

在这里插入图片描述
上述代码在多线程中使用SimpleDateFormat对象实例来解析字符串类型的日期。执行上述代码,一般来说,很可能出现一些异常(篇幅有限不再给出堆栈,只给出异常名称):
在这里插入图片描述

在这里插入图片描述
一种可行的解决方案是在sdf.parse()方法前后加锁,这也是我们一般的处理思路。这里不这么做,我们使用ThreadLocal为每一个线程创造一个SimpleDateformat对象实例:
在这里插入图片描述
在上述代码第7~9行中,如果当前线程不持有SimpleDateformat对象实例,那么就新建一个对象实例并把它置于当前线程中,如果已经持有,则直接使用。

从这里也可以看到,为每一个线程分配一个对象的工作并不是由ThreadLocal来完成的,而是需要在应用层面保证的。如果在应用中为每一个线程分配相同的对象实例,那么ThreadLocal也不能保证线程安全,这一点也需要大家注意。

**注意:**为每一个线程分配不同的对象,需要在应用层面保证ThreadLocal只起到了简单的容器作用。

2、ThreadLocal的实现原理

ThreadLocal如何保证对象实例只被当前线程访问呢?下面让我们一起深入ThreadLocal的内部实现。

我们需要关注的自然是ThreadLocal的set()方法和get()方法。先从set()方法说起:
在这里插入图片描述
在set()方法中,首先获得当前线程对象,然后通过getMap()方法获取线程的ThreadLocalMap,并将值存入ThreadLocalMap中。而ThreadLocalMap可以理解为一个Map(虽然不是,但是你可以把它简单地理解成HashMap),它是定义在Thread内部的成员。注意下面的定义是从Thread类中摘出来的:
在这里插入图片描述
设置到ThreadLocal中的数据,也就是写入了threadLocals的这个Map。其中,key为ThreadLocal当前对象,value就是我们需要的值。而threadLocals本身保存了当前自己所在线程的所有“局部变量”,也就是一个ThreadLocal变量的集合。

在get()方法中,自然就要将这个Map中的数据拿出来:
在这里插入图片描述
get()方法先取得当前线程的ThreadLocalMap对象,然后将自己作为key来取得内部的实际数据。

在了解了ThreadLocal的内部实现后,我们自然会引出一个问题:这些变量是维护在Thread类内部的(ThreadLocalMap定义所在类),这也意味着只要线程不退出,对象的引用将一直存在。

当线程退出时,Thread类会进行一些清理工作,其中就包括清理ThreadLocalMap,注意下述代码的加粗部分:
在这里插入图片描述
因此,使用线程池就意味着当前线程未必会退出(比如固定大小的线程池,线程总是存在)。如果这样,将一些大的对象设置到ThreadLocal中(它实际保存在线程持有的threadLocalMap内),可能会使系统出现内存泄漏(这里的意思是:你设置了对象到ThreadLocal中,但是不清理它,在你使用几次后,这个对象也不再有用了,但是它却无法被回收)。

此时,如果你希望及时回收对象,最好使用ThreadLocal.remove()方法将这个变量移除。就像我们习惯性地关闭数据库连接一样,如果你确实不需要这个对象了,就应该告诉虚拟机把它回收,防止内存泄漏。

另外一种有趣的情况是,JDK也可能允许你像释放普通变量一样释放ThreadLocal。比如,我们有时候为了加速垃圾回收,会特意写出类似obj=null的代码。如果这么做,obj所指向的对象就会更容易地被垃圾回收器发现,从而加速回收。

同理,对于ThreadLocal的变量,如果我们也手动将其设置为null,比如tl=null,那么这个ThreadLocal对应的所有线程的局部变量都有可能被回收。这里面的奥秘是什么呢?先来看一个简单的例子:
在这里插入图片描述
在这里插入图片描述
上述案例是为了跟踪ThreadLocal对象及内部SimpleDateFormat对象的垃圾回收情况,我们在第3行代码和第17行代码中重载了finalize()方法。这样,在对象被回收时,我们就可以看到它们的踪迹。

在主函数main()中,先后进行了两次任务提交,每次10000个任务。在第一次任务提交后,在代码的第39行,我们将tl设置为null,并执行一次GC。接着,我们进行第二次任务提交,完成后,在代码的第50行再执行一次GC。
在这里插入图片描述
注意这些输出所代表的含义。首先,线程池中的10个线程都各自创建了一个SimpleDateFormat对象实例。接着执行第一次GC,可以看到ThreadLocal对象被回收了(这里使用了匿名类,所以类名看起来有点怪,这个类就是第2行创建的tl对象)。然后提交第2次任务,这次一样创建了10个SimpleDateFormat对象,接着执行第二次GC。在第二次GC后,第一次创建的10个SimpleDateFormat的子类实例全部被回收。虽然我们没有手动移除这些对象,但是系统依然有可能回收它们。

要了解这里的回收机制,我们需要更进一步了解Thread.ThreadLocalMap的实现。我们之前说过,ThreadLocalMap类似HashMap,准确地说,它更加类似WeakHashMap。

ThreadLocalMap的实现使用了弱引用。弱引用是比强引用弱得多的引用。Java虚拟机在进行垃圾回收时,如果发现弱引用,就会立即回收。ThreadLocalMap内部由一系列entry构成,每一个entry都是WeakReference。
在这里插入图片描述
这里的参数k就是Map的key, v就是Map的value,其中k也是ThreadLocal实例,作为弱引用使用(super(k)就是调用了WeakReference的构造函数)。虽然这里使用ThreadLocal作为Map的key,但是实际上,它并不真的持有ThreadLocal的引用。而当ThreadLocal的外部强引用被回收时,ThreadLocalMap中的key就会变成null。当系统进行ThreadLocalMap清理时(比如将新的变量加入表中,就会自动执行一次清理,虽然JDK不一定会进行一次彻底的扫描,但显然在这个案例中,它奏效了),就会将这些垃圾数据回收。ThreadLocal的回收机制如下图所示:
在这里插入图片描述

3、对性能有何帮助

为每一个线程分配一个独立的对象对系统性能也许是有帮助的。当然,这也不一定,这完全取决于共享对象的内部逻辑。如果共享对象对于竞争的处理容易引起性能损失,我们还是应该考虑使用ThreadLocal为每个线程分配单独的对象。一个典型的案例就是在多线程下产生随机数。

这里,让我们简单测试一下在多线程下产生随机数的性能问题。首先,定义一些全局变量:
在这里插入图片描述
代码第1行定义了每个线程要产生的随机数数量;第2行定义了参与工作的线程数量;第3行定义了线程池;第4行定义了被多线程共享的Random实例,用于产生随机数;第6~11行定义了由ThreadLocal封装的Random。

定义一个工作线程的内部逻辑,它可以工作在两种模式下:

  • 第一种是多个线程共享一个Random(mode=0)。
  • 第二种是为多个线程各分配一个Random(mode=1)。
    在这里插入图片描述
    上述代码的第19~27行定义了线程的工作内容。每个线程都会产生若干个随机数,完成工作后,记录并返回所消耗的时间。

最后是main()函数,它分别对上述两种情况进行测试,并打印了耗时:
在这里插入图片描述
上述代码的运行结果可能如下:
在这里插入图片描述
很明显,在多线程共享一个Random实例的情况下,总耗时为13秒多(这里是指4个线程的耗时总和,不是程序执行经历的时间)。而在ThreadLocal模式下,仅耗时约1.7秒。

4、线程私有的随机数发生器ThreadLocalRandom

为了提高在高并发环境中随机数的产生效率,JDK提供了ThreadLocalRandom类。这是一个线程安全的随机数发生器。它让每个线程都维护一个自己的种子变量,每个线程生成随机数时都根据自己老的种子计算新的种子,再根据新的种子计算随机数,因此不存在竞争问题,从而提高了并发性能。

ThreadLocalRandom继承自Random,拥有Random的全部功能,只不过它运行更快、功能更强大。

在ThreadLocal的介绍中,我们已经知道,ThreadLocal的实现依赖于Thread对象中的’ThreadLocal.ThreadLocalMap threadLocals’成员字段。与之类似,为了让随机数发生器只访问本地线程数据,从而避免竞争,Thread中又增加了3个字段:
在这里插入图片描述
这3个字段作为Thread类的成员,便自然地和每一个Thread对象牢牢捆绑在一起,成了名副其实的ThreadLocal变量,而依赖这几个变量实现的随机数发生器,也就成了ThreadLocalRandom。

上述代码中,@sun.misc.Contended(“tlr”)表示这是一个消除伪共享的字段。消除伪共享可以提升字段的访问速度。

4.1、反射的高效替代方案

随机数的产生需要访问Thread的threadLocalRandomSeed等成员,但是考虑到类的封装性,这些成员只是包内可见的。很不幸,ThreadLocalRandom位于java.util.concurrent包,而Thread则位于java.lang包,因此,ThreadLocalRandom并没有办法访问Thread的threadLocalRandomSeed等变量。

这时,Java老鸟们可能就会跳出来说:“这算什么,看我的反射大法,不管啥都能抠出来访问一下。”说得没错,反射是一种可以绕过封装直接访问对象内部数据的方法,但是,反射的性能不太好,并不适合作为高性能的解决方案。有没有可以让ThreadLocalRandom访问Thread的内部成员,同时又远超于反射且无限接近于直接访问变量的方法呢?答案是肯定的,这就是使用Unsafe类。

这里简单介绍一下Unsafe类的两个方法:
在这里插入图片描述
其中getLong()方法会读取对象o的第offset字节偏移量的一个long型数据;putLong()方法则会将x写入对象o的第offset个字节的偏移量中。这种类似C语言的操作方法,带来了极大的性能提升,更重要的是,由于它避开了字段名,直接使用偏移量,可以轻松绕过成员的可见性限制。

性能问题解决了,下一个问题是:我怎么知道threadLocalRandomSeed成员在Thread中的偏移位置呢?这就需要用Unsafe类的objectFieldOffset()方法了,请看下面的代码:
在这里插入图片描述
上述这段代码,在ThreadLocalRandom类初始化的时候,就取得了Thread成员变量threadLocalRandomSeed、threadLocalRandomProbe和threadLocalRandomSecondarySeed在对象偏移中的位置。因此,只要ThreadLocalRandom需要使用这些变量,都可以通过Unsafe类的getLong()和putLong()方法来访问(也可能是getInt()和putInt()方法)。

比如像下面一样生成一个随机数的时候:
在这里插入图片描述
这种Unsafe类的方法到底能有多快呢?根据笔者的经验,这比传统的反射至少快3倍。这也是JDK内部大量使用Unsafe类的方法而非反射的一个重要原因。

4.2、随机数种子

伪随机数生成都需要一个种子,threadLocalRandomSeed和threadLocalRandomSecondary-Seed就是这里的种子。其中threadLocalRandomSeed是long型的,threadLocalRandomSecondary-Seed是int型的。threadLocalRandomSeed是使用最广泛的。大量的随机数其实都是基于threadLocalRandomSeed的。而threadLocalRandomSecondarySeed只在某些特定的JDK内部实现中使用,使用并不广泛。

初始种子默认使用的是系统时间:
在这里插入图片描述
上述代码完成了种子的初始化,并将初始化的种子通过UNSAFE储存在SEED的位置(即threadLocalRandomSeed)。

接着就可以使用nextInt()方法获得随机整数了:
在这里插入图片描述
每一次调用nextInt()方法都会使用nextSeed()方法更新threadLocalRandomSeed。由于这是一个线程独有的变量,因此完全不会有竞争,也不会有CAS重试,性能也就大大提高了。

4.3、探针Probe的作用

除了种子,还有一个threadLocalRandomProbe探针变量,这个变量是用来做什么的呢?我们可以把threadLocalRandomProbe理解为针对每个Thread的Hash值(不为0),它可以作为一个线程的特征值,基于这个值可以为线程在数组中找到一个特定的位置:
在这里插入图片描述
来看下面的代码片段:
在这里插入图片描述
在具体的实现中,如果上述代码发生了冲突,还可以使用ThreadLocalRandom.advance-Probe()方法来修改一个线程的探针值,这样可以进一步避免未来可能出现的冲突,从而减少竞争,提高并发性能。
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2024年数学建模竞赛汇总——时间轴

美赛已过&#xff0c;好多小伙伴表示已经错过&#xff0c;不清楚什么时候报名&#xff0c;什么时候准备&#xff0c;其实每年数学建模比赛有很多个&#xff0c;各大比赛的级别、报名时间、参赛对象等要求什么呢&#xff1f;小编从竞赛说明、竞赛级别、是否允许跨校、报名费用、…

MySQL基础学习

MySQL基础 注意&#xff1a;本文的图片截图自尚硅谷MySQL笔记。 一&#xff1a;基本概述&#xff1a; 什么是数据库&#xff1a; 数据库是一种用来存储和管理数据的系统。它是一个组织化的数据集合&#xff0c;可以通过计算机系统进行访问、管理和更新。数据库可以存储各种…

【C++】vector模拟实现+迭代器失效

vector模拟实现 成员变量定义默认成员函数构造函数 迭代器范围for、对象类型匹配原则 容量操作sizeemptycapacityreserve成员变量未更新memcpy值拷贝 resize内置类型的构造函数 数据访问frontbackoperator[ ] 数据修改操作push_backpop_backswapclearinsertpos位置未更新无返回…

Atcoder ABC340 C - Divide and Divide

Divide and Divide&#xff08;分而治之&#xff09; 时间限制&#xff1a;2s 内存限制&#xff1a;1024MB 【原题地址】 所有图片源自Atcoder&#xff0c;题目译文源自脚本Atcoder Better! 点击此处跳转至原题 【问题描述】 【输入格式】 【输出格式】 【样例1】 【样例…

Unity2023.1.19没有PBR Graph?

Unity2023.1.19没有PBR Graph? 关于Unity2023.1.19没有PBR graph的说法,我没看见管方给出的答案,百度则提到了Unity2020版之后Shader Graph的“全新更新”,之前也没太注意版本的区别,以后项目尽量都留心一下。 之前文章说过,孪生智慧项目推荐使用URP渲染管线,以上的截…

基于学习的参数化查询优化方法

一、背景介绍 参数化查询是指具有相同模板&#xff0c;且只有谓词绑定参数值不同的一类查询&#xff0c;它们被广泛应用在现代数据库应用程序中。它们存在反复执行动作&#xff0c;这为其性能优化提供了契机。 然而&#xff0c;当前许多商业数据库处理参数化查询的方法仅仅只…

JavaScript编写一个倒计时

关键js代码 <script>function countdown() {var targetDate new Date("2024-02-20 12:00:00");var currentDate new Date();var timeDiff targetDate.getTime() - currentDate.getTime();var days Math.floor(timeDiff / (1000 * 60 * 60 * 24));var hours…

电商数据API接口 | 节省你的电商数据采集成本

1、将数据采集的整体成本降低55%。在电商API接口负责了整个数据采集流程后&#xff0c;这家电商公司成功节约了维护和开发上的成本。 2、电商爬虫API可以从极复杂的来源中采集数据&#xff0c;确保完整交付。在电商爬虫API的帮助下&#xff0c;该公司现在可以获取完成业务目标所…

threejs 3D标注

import { CSS3DObject } from "three/examples/jsm/renderers/CSS3DRenderer";gltfLoader.load("./model/exhibit2.glb", (gltf) >{let array ["雕像", "中药房", "浸制区", "道地沙盘","动物标本区&quo…

Java的Lock(二)

自旋锁 VS 适应性自旋锁 堵塞或者notify一个Java线程需要操作系统切换CPU状态来完成(详情请参考11408)。这种状态切换需要耗费CPU时间。如果同步代码块种的内容过于简单。状态切换消耗的时间可能比用户代码执行的时间还要长。 在许多场景中,同步资源的锁定时间很短,为了这一…

【C语言】Leetcode 88.合并两个有序数组

一、代码实现 /*** 函数名称&#xff1a;merge* * 功能描述&#xff1a;合并两个已排序的整数数组* * 参数说明&#xff1a;* nums1&#xff1a;第一个整数数组* nums1Size&#xff1a;第一个数组的大小* m&#xff1a;第一个数组中要合并的子数组的起始索引* nums2&a…

MyBatis Plus:自定义typeHandler类型处理器

目录 引言&#xff1a;关于TypeHandler PostGreSQL&#xff1a;JSON数据类型 PostGreSQL数据库驱动&#xff1a;PGobject类 TypeHandler类型处理器 自定义类型处理器 类型处理器实现&#xff1a;PGJsonTypeHandler 注册类型处理器 引言&#xff1a;关于TypeHandler MyBa…

普中51单片机学习(十四)

中断系统 中断的概念 CPU在处理某一事件A时&#xff0c;发生了另一事件B请求CPU迅速去处理&#xff08;中断发生&#xff09;,CPU暂时中断当前的工作&#xff0c;转去处理事件B&#xff08;中断响应和中断服务)&#xff0c;待CPU将事件B处理完毕后&#xff0c;再回到原来事件…

【C语言的小角落】逻辑与逻辑或混合计算

关注小庄 顿顿解馋(≧◡≦) 引言&#xff1a;本篇博客小庄带领小伙伴们解决一个比较角落有时头疼的问题—关于逻辑与和逻辑或结合运算的问题&#xff0c;请放心食用~ 我们先放代码说话 int main() {int x 1;int y 3;int z 4;if(x1 || y && z){;} printf("y …

【医学大模型】Text2MDT :从医学指南中,构建医学决策树

Text2MDT &#xff1a;从医学指南中&#xff0c;构建医学决策树 提出背景Text2MDT 逻辑Text2MDT 实现框架管道化框架端到端框架 效果 提出背景 论文&#xff1a;https://arxiv.org/pdf/2401.02034.pdf 代码&#xff1a;https://github.com/michael-wzhu/text2dt 假设我们有一…

使用Sora部署实时音视频通信应用实战项目

一、项目概述 本项目将构建一个在线教学平台&#xff0c;实现教师与学生之间的实时音视频通信。平台将提供教师上传课件、发起授课邀请&#xff0c;学生加入课堂、实时互动等功能。通过使用Sora&#xff0c;我们将确保音视频通信的稳定、流畅和低延迟。 目录 一、项目概述 二…

并发编程线程安全之同步锁Synchronized

一、原子性定义 原子性的本质是互斥访问&#xff0c;同一时刻只有一个线程对它进行访问操作 二、原子性问题的简述 public class AutomicDemo {int count 0;public static void main(String[] args) throws InterruptedException {AutomicDemo automicDemo new AutomicDem…

洛谷C++简单题小练习day15—计算阶乘小程序(不用循环)

day15--计算阶乘小程序--2.19 习题概述 题目描述 求 n!&#xff0c;也就是 123⋯n。 挑战&#xff1a;尝试不使用循环语句&#xff08;for、while&#xff09;完成这个任务。 输入格式 第一行输入一个正整数 n。 输出格式 输出一个正整数&#xff0c;表示 n! 代码部分 …

MySQL数据库基础(十一):多表查询

文章目录 多表查询 一、交叉连接&#xff08;了解&#xff09; 二、内连接 1、连接查询的介绍 2、内连接查询 3、小结 三、左外连接 1、左连接查询 2、小结 四、右外连接 1、右连接查询 2、小结 多表查询 一、交叉连接&#xff08;了解&#xff09; 它是所有连接…

Elasticsearch:将 IT 智能和业务 KPI 与 AI 连接起来 - 房间里的大象

作者&#xff1a;Fermi Fang 大象寓言的智慧 在信息技术和商业领导力的交叉点&#xff0c;蒙眼人和大象的古老寓言提供了一个富有洞察力的类比。 这个故事起源于印度次大陆&#xff0c;讲述了六个蒙住眼睛的人第一次遇到大象的故事。 每个人触摸大象的不同部位 —— 侧面、象牙…