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

文章目录

  • 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;}@Overridepublic 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.rhkb.cn/news/260403.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

继续教育公需科目试题及答案,分享几个实用搜题和学习工具 #经验分享#经验分享

大学生活是一个充满挑战和机遇的阶段&#xff0c;在这个阶段&#xff0c;我们需要不断提升自己的学习能力和技巧。而寻找适合自己的学习工具也成为了我们必须面对的任务。幸运的是&#xff0c;现在有许多日常学习工具可以帮助我们更好地组织学习、提高效率。今天&#xff0c;我…

SQL Developer 小贴士:显示Trace文件

SQL Developer可以识别trace文件&#xff0c;而无需利用tkprof进行转换。 在数据库服务器上生产trace文件。例如&#xff1a; alter session set tracefile_identifierdemo01_02;alter session set sql_tracetrue;-- your SQL here, for example select * from hr.employees;a…

什么是渲染?渲染有几种类型?渲染100邀请码1a12

渲染是CG作业的最后一步&#xff0c;根据分类依据不同&#xff0c;有以下几个类型&#xff1a; 1、操作响应 根据对渲染结果的响应要求和实现原理不同&#xff0c;渲染可分为离线渲染、实时渲染和混合渲染。离线渲染通常在本地进行&#xff0c;由电脑生成画面&#xff0c;时间从…

TimeDad 简单的PC使用时间控制软件

TimeDad 起因 过年教家里的小朋友玩我的世界&#xff0c;这家伙着了魔&#xff0c;每天霸着电脑&#xff0c;说梦话都是挖矿。 找了time boss破解版用了一段时间&#xff0c;破解失效了。找了一圈软件发现功能都好复杂&#xff0c;要收费的&#xff0c;没办法&#xff0c;娃…

MySQL中SQL语句的执行流程(高频考点)

文章目录 前言SQL语句的执行流程查询语句的执行流程更新语句的执行流程 总结 前言 昨天跟大家讲了MySQL的基础架构&#xff08;链接&#xff1a;MySQL的基础架构&#xff09;&#xff0c;今天讲一讲我们的高频面试题MySQL中SQL语句的执行流程。 建议看完 MySQL的基础架构 再来…

二维红外流程

x.1 开激光器 先将TDG&#xff0c;TCU&#xff0c;Empower打开&#xff0c;等一分钟后将TDG和Empower的钥匙打到On上&#xff1b; 按顺序先后开MaiTai&#xff1b;ACE&#xff1b;TOPAS&#xff1b;AOM&#xff1b; 测量ACE出光口处功率&#xff08;3.8w&#xff09;&#x…

红队打靶练习:IMF: 1

目录 信息收集 1、arp 2、nmap 3、nikto 目录探测 gobuster dirsearch WEB 信息收集 get flag1 get flag2 get flag3 SQL注入 漏洞探测 脱库 get flag4 文件上传 反弹shell 提权 get flag5 get flag6 信息收集 1、arp ┌──(root㉿ru)-[~/kali] └─# a…

java+vue_springboot企业设备安全信息系统14jbc

企业防爆安全信息系统采用B/S架构&#xff0c;数据库是MySQL。网站的搭建与开发采用了先进的java进行编写&#xff0c;使用了vue框架。该系统从三个对象&#xff1a;由管理员、人员和企业来对系统进行设计构建。主要功能包括&#xff1a;个人信息修改&#xff0c;对人员管理&am…

爬虫入门一

文章目录 一、什么是爬虫&#xff1f;二、爬虫基本流程三、requests模块介绍四、requests模块发送Get请求五、Get请求携带参数六、携带请求头七、发送post请求八、携带cookie方式一&#xff1a;放在请求头中方式二&#xff1a;放在cookie参数中 九、post请求携带参数十、模拟登…

C++11---(3)

目录 一、可变参数模板 1.1、可变参数模板的概念 1.2、可变参数模板的定义方式 1.3、如何获取可变参数 二、lambda表达式 2.1、Lamabda表达式定义 2.2、为什么有Lambda 2.3、Lambda表达式的用法 2.4、函数对象与lambda表达式 三、包装器 3.1、function 3.2、bind …

智慧公厕是什么?智慧公厕对智慧城市的意义

城市的信息化发展需要催化了智慧城市&#xff0c;公共厕所作为城市的重要民生设施&#xff0c;如何实现更高阶的信息化建设&#xff0c;成为一个重要课题。那么&#xff0c;智慧公厕是什么&#xff1f;为什么它对智慧城市的建设如此重要&#xff1f;本文以智慧公厕源头厂家广州…

前端新手Vue3+Vite+Ts+Pinia+Sass项目指北系列文章 —— 第十二章 常用工具函数 (Utils配置)

前言 在项目开发中&#xff0c;我们经常会使用一些工具函数&#xff0c;也经常会用到例如loadsh等工具库&#xff0c;但是这些工具库的体积往往比较大&#xff0c;如果项目本身已经引入了这些工具库&#xff0c;那么我们就没有必要再引入一次&#xff0c;所以我们需要自己封装…

如何利用Idea创建一个Servlet项目(新手向)

&#x1f495;"Echo"&#x1f495; 作者&#xff1a;Mylvzi 文章主要内容&#xff1a;如何利用Idea创建一个Servlet项目(新手向) Servlet是tomcat的api,利用Servlet进行webapp开发很方便,本文将介绍如何通过Idea创建一个Servlet项目(一共分为七步,这可能是我们写过的…

微软和OpenAI将检查AI聊天记录,以寻找恶意账户

据国外媒体报道&#xff0c;大型科技公司及其附属的网络安全、人工智能产品很可能会推出类似的安全研究&#xff0c;尽管这会引起用户极度地隐私担忧。大型语言模型被要求提供情报机构信息&#xff0c;并用于帮助修复脚本错误和开发代码以侵入系统&#xff0c;这将很可能会成为…

用tensorflow模仿BP神经网络执行过程

文章目录 用矩阵运算仿真BP神经网络y relu ( (X․W ) b )y sigmoid ( (X․W ) b ) 以随机数产生Weight(W)与bais(b)placeholder 建立layer函数改进layer函数&#xff0c;使其能返回w和b github地址https://github.com/fz861062923/TensorFlow 用矩阵运算仿真BP神经网络 impo…

路径相关树形dp——最长乘积链

路径相关树形dp——最长乘积链 问题描述 给定一棵树&#xff0c;树中包含n个结点&#xff0c;编号为1~n&#xff0c;以及n- 1条无向边&#xff0c;每条边都有一个权值。 现从树中任选一个点&#xff0c; 从该点出发&#xff0c;在不走回头路的情况下找出二条到其他点的路径&…

keil5代码复制下来中文乱码

在keil5中&#xff0c;显示正常&#xff0c;如果复制到其他编辑器&#xff0c;中文部分就乱码。 keil5中显示正常 复制到其他编辑器&#xff0c;中文乱码。 原因&#xff1a;编码格式不一样 解决办法&#xff1a;keil5中重新设置一下。 左边选择Edit&#xff0c;下面选择Conf…

第3.3章:StarRocks数据导入--Stream Load

一、概述 Stream Load是StarRocks常见的数据导入方式&#xff0c;用户通过发送HTTP请求将本地文件或数据流导入至StarRocks中&#xff0c;该导入方式不依赖其他组件。 Stream Load作是一种同步导入方式&#xff0c;可以直接通过请求的返回值判断导入是否成功&#xff0c;无法手…

考PMP真的有用吗?

在你决定考证之前&#xff0c;值得思考的是为什么要追求这个证书。是因为公司的需求&#xff1f;个人职业发展&#xff1f;还是受到了新闻报道或广告的影响&#xff0c;觉得PMP证书有价值&#xff0c;只是出于好奇想了解一下。这种情况下&#xff0c;很多人可能会表示&#xff…

idea代码review工具Code Review Helper使用介绍

之前在团队里面遇到一个关于代码review的问题&#xff0c;使用gitlab自己的还是facebook的Phabricator&#xff0c;很难看到整体逻辑&#xff0c;因为业务逻辑代码可能不在这次改动范围内&#xff0c;在去源库中找不好找。针对这个刚需&#xff0c;在网上找了一个idea的代码工具…