并发编程1:线程安全性概述
目录
1、什么是线程安全性?
2、操作的原子性:避免竞态条件
3、锁机制:内置锁和可重入
4、如何用锁来保护状态?
5、同步机制中的活跃性与性能问题
编写线程安全的代码,其核心在于对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问。//核心:对共享并且可变状态进行管理
对象的状态是指存储在状态变量中的数据。状态变量可以是类的实例或成员变量。
一个对象是否需要是线程安全的,取决于它是否被多个线程访问。要使得对象是线程安全的,需要采用同步机制来协同对对象可变状态的访问。如果无法实现协同,那么可能会导致数据破坏以及其他不该出现的结果。//通过同步实现对象的线程安全
Java 中的主要同步机制是关键字 synchronized,它提供了一种独占的加锁方式,此外,还包括 volatile 类型的变量,显式锁 (Explicit Lock)以及原子变量等。//解决同步中的可见性和原子性(顺序)
1、什么是线程安全性?
当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。//所见即所知
public class StatelessFactorizer extends GenericServlet implements Servlet {public void service(ServletRequest req, ServletResponse resp) {//1-从req中获取值BigInteger i = extractFromRequest(req);BigInteger[] factors = factor(i);//2-编码并响应encodeIntoResponse(resp, factors);}
}
上述 StatelessFactorizer 是无状态的:它既不包含任何成员变量,也不包含任何对其他类中成员变量的引用。计算过程中的临时状态仅存在于线程栈上的局部变量中,并且只能由正在执行的线程访向。由于线程访问无状态对象的行为并不会影响该对象在其他线程中操作的正确性,因此无状态对象是线程安全的。//不进行共享就不存在线程安全问题
2、操作的原子性:避免竞态条件
假设我们想增加一个“命中计数器”(Hit Counter) 来统计所处理的请求数量。最简单的方法是在 Servlet 中增加一个 long 类型的成员变量,并且每处理一个请求就将这个值加 1,代码如下:
//存在线程安全问题
public class UnsafeCountingFactorizer extends GenericServlet implements Servlet {//计数器private long count = 0;public long getCount() {return count;}public void service(ServletRequest req, ServletResponse resp) {//1-从req中获取值BigInteger i = extractFromRequest(req);BigInteger[] factors = factor(i);++count;//2-编码并响应encodeIntoResponse(resp, factors);}
}
我们都知道,虽然递增操作 ++count 看上去只是一个操作,但这个操作并非原子的。实际上,它包含了三个独立的操作:读取 count 的值,将值加 1,然后将计算结果写入 count。这是一个 “读取 - 修改 - 写” 的操作序列,并且其结果状态依赖于之前的状态。//指令是非原子性的,如果步骤乱了,结果也会乱
在上述 UnsafeCountingFactorizer 中存在多个竞态条件,从而使结果变得不可靠。最常见的竞态条件就是 “先检查后执行 (Check-Then-Act)” 操作,即通过一个可能失效的观测结果来决定下一步的动作。//如果前提条件是错误的,那么论证的结果一般也是错误的
比如,首先观察到某个条件为真(例如文件 X 不在),然后根据这个观察结果执行用应的动作(创建文件X),但事实上,在你观察到这个结果以及开始创建文件之间,观察结果可能变得无效(另一个线程在这期间创建了文件 X),从而导致各种问题(未预期的异常数据被覆盖、文件被破等)。//单例问题
为了确保线程安全性,避免竞态条件,“先检查后执行”和“读取 - 修改 - 写入”等操作必须是原子的。
public class CountingFactorizer extends GenericServlet implements Servlet {//使用原子类private final AtomicLong count = new AtomicLong(0);public long getCount() { return count.get(); }public void service(ServletRequest req, ServletResponse resp) {BigInteger i = extractFromRequest(req);BigInteger[] factors = factor(i);count.incrementAndGet();encodeIntoResponse(resp, factors);}
}
在实际情况中,应尽可能地使用现有的线程安全对象(例如 AcomicLong)来管理类的状态。与非线程安全的对象相比,判断线程安全对象的状态及其状态转换情况要更加容易,从而也更容易维护和验证线程安全性。//仅对单个变量的安全性有效
3、锁机制:内置锁和可重入
面对多个变量时,原子类并不能保证同步机制有效:
//存在线程安全问题
public class UnsafeCachingFactorizer extends GenericServlet implements Servlet {//原子类变量1private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();//原子类变量2private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();public void service(ServletRequest req, ServletResponse resp) {BigInteger i = extractFromRequest(req);//两个变量不能保证同时获取或者同时设置if (i.equals(lastNumber.get())) //获取变量1的值encodeIntoResponse(resp, lastFactors.get()); //获取变量2的值else {BigInteger[] factors = factor(i);lastNumber.set(i); //设置变量1的值lastFactors.set(factors); //设置变量2的值encodeIntoResponse(resp, factors);}}
}
此时,就需要引入锁机制来确保线程的同步。
Java 提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。每个Java 对象都可以用做一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。//就是所谓的管程,Synchronized 太常见就不过多介绍了
Synchronized 的问题:使用同步代码块,很容易对代码进行过于极端的保护,虽然解决了安全问题,但带来了性能问题。//锁的粗粒度和细粒度问题
内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重人”意味着获取锁的操作的粒度是“线”,而不是“调用”。//不可重入会造成自己阻塞自己的问题
重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为 0 时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM 将记下锁的持有者,并且将获取计数值置为 1。如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时计数器会相应地递减。当计数值为 0 时,这个锁将被释放。//重入锁的实现原理
4、如何用锁来保护状态?
锁能使其保护的代码以串行形式来访问,因此可以通过锁来实现对共享状态的独占访问。
下边是一些正确使用锁的建议:
(1)如果用同步来协调对某个变量的访问,那么在访问和操作这个变量的所有位置上都需要使用同步。而且,在访问和操作变量的所有位置上都要使用同一个锁。//对共享变量的读写都要上锁
之所以每个对象都有一个内置锁,只是为了免去显式地创建锁对象。你可以自行构造加锁协议或者同步策略来实现对共享状态的安全访问,并且在程序中自始至终地使用它们。
(2)每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。
(3)对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。
5、同步机制中的活跃性与性能问题
试想,如果同步可以避免竞态条件问题,那么为什么不在每个方法声明时都使用关键字 synchronized 呢?
事实上,如果不加区别地用 synchronized,可能导致程序中出现过多的同步。此外,如果只是将每个方法都作为同步方法,例如 Vector,那么并不足以确保 Vector 上复合操作都是原子的:
//非原子操作
if (!vector.contains(element))vector.add(element);
此外,将每个方法都作为同步方法还可能导致活跃性问题(Liveness)或性能问题(Performance)。
如下代码,如果使用 SynchronizedFactorizer 中的同步方式,那么代码的执行性能将非常糟糕。//不能直接把方法一锁了之,虽然实现了线程安全,但付出了太大性能代价
//线程安全
public class SynchronizedFactorizer extends GenericServlet implements Servlet {//成员变量private BigInteger lastNumber;private BigInteger[] lastFactors;//直接锁方法,存在性能问题public synchronized void service(ServletRequest req, ServletResponse resp) {BigInteger i = extractFromRequest(req);if (i.equals(lastNumber))encodeIntoResponse(resp, lastFactors);else {BigInteger[] factors = factor(i);lastNumber = i;lastFactors = factors;encodeIntoResponse(resp, factors);}}
}
锁优化思路:缩小同步代码块的作用范围,做到既确保 Servlet 的并发性,同时又维护线程安全性。要确保同步代码块不要过小,并且不要将本应是原子的操作拆分到多个同步代码块中。应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其他线程可以访问共享状态。//将粗粒度的锁尽量缩小,将执行时间长的代码进行剥离
重新构造后的 CachedFactorizer 实现了在简单性与并发性 之间的平衡。代码如下:
//线程安全
public class CachedFactorizer extends GenericServlet implements Servlet {//共享变量private BigInteger lastNumber;private BigInteger[] lastFactors;//命中计数器private long hits;//cache命中计数器private long cacheHits;public synchronized long getHits() {return hits;}public synchronized double getCacheHitRatio() {return (double) cacheHits / (double) hits;}public void service(ServletRequest req, ServletResponse resp) {//1-从req获取值BigInteger i = extractFromRequest(req);BigInteger[] factors = null; synchronized (this) { //同步代码块1,对变量进行操作++hits;if (i.equals(lastNumber)) {++cacheHits;factors = lastFactors.clone();}}if (factors == null) {factors = factor(i); //局部变量,不需要进行同步synchronized (this) { //同步代码块2,对变量进行操作lastNumber = i;lastFactors = factors.clone();}}//2-响应:把执行时间长的代码进行剥离encodeIntoResponse(resp, factors);}void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {}BigInteger extractFromRequest(ServletRequest req) {return new BigInteger("7");}BigInteger[] factor(BigInteger i) {// Doesn't really factorreturn new BigInteger[]{i};}
}
在 CachedFactorizer 中不再使用 AtomicLong 类型的命中计数器,而是使用了一个 long 类型的变量。当然也可以使用 AtomicLong 类型,对在单个变量上实现原子操作来说,原子变量非常有用。但此处,由于我们已经使用了同步代码块来构造原子操作,而使用两种不同的同步机制不仅会带来混乱,也不会在性能或安全性上带来任何好处,因此在这里不使用原子变量。//同一个类中,应该只使用一种同步机制,让代码简单易懂。
要判断同步代码块的合理大小,需要在各种设计需求之间进行权衡,包括安全性(必须满足)、简单性和性能。有时候,在简单性与性能之间会发生冲突,但在二者之间通常能找到某种合理的平衡。通常,在简单性与性能之间存在着相互制约因素。当实现某个同步策略时,一定不要盲目地为了性能而牺牲简单性 (这可能会破坏安全性)。//努力做到安全性和性能的平衡
无论是执行计算密集的操作,还是在执行某个可能阻塞的操作,如果持有锁的时间过长,那么都会带来活跃性或性能问题。所以,当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络I/O 或控制台I/O),一定不要持有锁。//执行时间较长的代码不要持有锁
至此,全文到此结束。
相关文章:

并发编程1:线程安全性概述
目录 1、什么是线程安全性? 2、操作的原子性:避免竞态条件 3、锁机制:内置锁和可重入 4、如何用锁来保护状态? 5、同步机制中的活跃性与性能问题 编写线程安全的代码,其核心在于对状态访问操作进行管理࿰…...

(论文复现)DeepAnt模型复现及应用
DeepAnt论文如下,其主要是用于时间序列的无监督粗差探测。 其提出的模型架构如下: 该文提出了一个无监督的时间序列粗差探测模型,其主要有预测模块和探测模块组成,其中预测模块的网络结构如下。 预测结构是将时间序列数据组…...

【机器学习】在 MLOps构建项目 ( MLOps2)
My MLOps tutorials: Tutorial 1: A Beginner-Friendly Introduction to MLOps教程 2:使用 MLOps 构建机器学习项目 一、说明 如果你希望将机器学习项目提升到一个新的水平,MLOps 是该过程的重要组成部分。在本文中,我们将以经典手写数字分类…...

【MySQL】聚合函数与分组查询
文章目录 一、聚合函数1.1 count 返回查询到的数据的数量1.2 sum 返回查询到的数据的总和1.3 avg 返回查询到的数据的平均值1.4 max 返回查询到的数据的最大值1.5 min 返回查询到的数据的最小值 二、分组查询group by2.1 导入雇员信息表2.2 找到最高薪资和员工平均薪资2.3 显示…...

conda 环境 numpy 安装报错需要 Microsoft Visual C++ 14.0
到公司装深度学校环境。项目较旧,安装依赖,一堆报错(基于 conda 环境): numpy 安装报需要 C 14.0 No module named numpy.distutils._msvccompiler in numpy.distutils; trying from distutilserror: Microsoft Visu…...

算法工程师-机器学习面试题总结(5)
什么是信息熵? 信息熵是信息理论中用来衡量一个随机变量的不确定度或者信息量的概念。它是在给定一组可能的事件中,对每个事件发生的概率进行加权平均得到的值。 在信息熵的计算中,概率越大的事件所带来的信息量越小,概率越小的事…...

论文阅读 RRNet: A Hybrid Detector for Object Detection in Drone-captured Images
文章目录 RRNet: A Hybrid Detector for Object Detection in Drone-captured ImagesAbstract1. Introduction2. Related work3. AdaResampling4. Re-Regression Net4.1. Coarse detector4.2. Re-Regression 5. Experiments5.1. Data augmentation5.2. Network details5.3. Tra…...

js执行机制
JavaScript 的执行机制是基于单线程的事件循环模型。这意味着 JavaScript 代码会按照顺序一行一行地执行,同时只能执行一个任务。让我们更详细地了解 JavaScript 的执行机制: 调用栈(Call Stack): JavaScript 使用调用…...

关于策略模式的注入问题
上面抄别人的 当在实现策略方法时,报null,排查后发现是接口实现有多个,需要添加别名 注入时添加Qeualifier,指定名称,如下图;如图上修改, 测试类中不用new具体行为策略了,注入别名即…...

通用Mapper的四个常见注解
四个常见注解 1、Table 作用:建立实体类和数据库表之间的对应关系。 默认规则:实体类类名首字母小写作为表名,如 Employee -> employee 表 用法:在 Table 注解的 name 属性中指定目标数据库的表名; 案例&#…...

二进制安装K8S(单Master集群架构)
目录 一:操作系统初始化配置 1、项目拓扑图 2、服务器 3、初始化操作 二: 部署 etcd 集群 1、etcd 介绍 2、准备签发证书环境 3、master01 节点上操作 (1)生成Etcd证书 (2)创建用于存放 etcd 配置文…...

基于java汽车销售分析与管理系统设计与实现
摘 要 计算机现在已成为人们办公和生活不可或缺的组成部分,在工作范畴计算机成熟运用大大提升了工作人员的工作效率,化繁为简,加速社会经济发展。在生活上,人们可以通过计算机互联网更快的了解到全球时事要闻、听到最新潮流音乐、…...

Glass指纹识别工具,多线程Web指纹识别工具-Chunsou
Glass指纹识别工具,多线程Web指纹识别工具-Chunsou。 Glass指纹识别工具 Glass一款针对资产列表的快速指纹识别工具,通过调用Fofa/ZoomEye/Shodan/360等api接口快速查询资产信息并识别重点资产的指纹,也可针对IP/IP段或资产列表进行快速的指…...

BIO,NIO,AIO总结
文章目录 1. BIO (Blocking I/O)1.1 传统 BIO1.2 伪异步 IO1.3 代码示例 1.4 总结2. NIO (New I/O)2.1 NIO 简介2.2 NIO的特性/NIO与IO区别1)Non-blocking IO(非阻塞IO)2)Buffer(缓冲区)3)Channel (通道)4)Selector (选择器) 2.3 NIO 读数据和写数据方式…...

[腾讯云Cloud Studio实战训练营]基于Cloud Studio完成图书管理系统
[腾讯云Cloud Studio实战训练营]基于Cloud Studio完成图书管理系统 ⭐前言🌜Cloud Studio产品介绍1.登录2.创建工作空间3.工作空间界面简介4.环境的使用 ⭐实验实操🌜Cloud Studio实现图书管理系统1.实验目的 2. 实验过程2.实验环境3.源码讲解3.1添加数据…...

(二)Node.js 基础模块
(二)Node.js 基础模块 1. fs文件系统模块1.1 什么是fs文件系统模块1.2 读取指定文件中的内容1. fs.readFile()的语法格式2. fs.readFile()的示例代码 1.3 向指定的文件中写入内容1. fs.writeFile()的语法格式2. fs.writeFile()的实例代码 1.4 __dirname …...

AUC及其拓展GAUC
AUC及其拓展GAUC auc的定义 auc用来评估一个分类器的排序质量,它的物理含义:给定一堆正负样本,随机取一个正样本,一个负样本,学习器将正样本排在负样本前面的概率 auc的计算 具体计算方法:给定m个正样本…...

【CSS】CSS 选择器
CSS 选择器 1.基础选择器 1.1 元素选择器 语法:标签名{...} 元素选择器会选中对应标签名的HTML元素,例如:p{...},div{...},span{...}等 1.2 类选择器 语法:.类名{...} 类选择器会选中class属性为指定…...

2023-08-07力扣今日四题-好题
链接: 剑指 Offer 03. 数组中重复的数字 题意: 如题 解: 看到一个很牛的时间复杂度O(n)的原地算法:由于数组长度n,数组内只有0到n-1,那么,我们用对应-n到-1表示nums[index]出现过一次&…...

Packet Tracer - IPv4 和 IPv6 编址故障排除
Packet Tracer - IPv4 和 IPv6 编址故障排除 地址分配表 设备 接口 IPv4 地址 子网掩码 默认网关 IPv6 地址/前缀 R1 G0/0 10.10.1.1 255.255.255.0 N/A G0/1 192.168.0.1 255.255.255.0 N/A 2001:DB8:1:1::1/64 N/A G0/2 2001:DB8:1:2::1/64 N/A S0/0/0 …...

PHP国外在线教育系统源码 在线课程系统源码 直播课程系统源码提供在线课程,现场课程,测验
Proacademy是在线教育一体化的解决方案,用于创建类似于Udemy、Skillshare、Coursera这种在线教育市场。 这个平台提供在线课程,现场课程,测验等等,并有一个基于实际业务需要的高级认证插件,程序基于Laravel强大的安全框…...

Abaqus 中最常用的子程序有哪些 硕迪科技
在ABAQUS中,用户定义的子程序是一种重要的构件,可以将其插入到Abaqus分析中以增强该软件的功能和灵活性。这些子程序允许用户在分析过程中添加自定义材料模型、边界条件、初始化、加载等特定操作,以便更精准地模拟分析中的现象和现象。ABAQUS…...

容器——3.Collection 子接口之 Set
文章目录 3.1. comparable 和 Comparator 的区别3.1.1. Comparator 定制排序3.1.2. 重写 compareTo 方法实现按年龄来排序 3.2. 无序性和不可重复性的含义是什么3.3. 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同 3.1. comparable 和 Comparator 的区别 comparable 接口…...

将整数,结构体,结构体数组,链表写到文件
在之前的学习中,忘文件中写的内容都是字符串或字符,本节学习如何写入其他各种类型的数据。 回看write和read函数的形式: ssize_t write(int fd, const void *buf, size_t count); ssize_t read(int fd, void *buf, size_t count); 其中&a…...

UNIX基础知识:UNIX体系结构、登录、文件和目录、输入和输出、程序和进程、出错处理、用户标识、信号、时间值、系统调用和库函数
引言: 所有的操作系统都为运行在其上的程序提供服务,比如:执行新程序、打开文件、读写文件、分配存储区、获得系统当前时间等等 1. UNIX体系结构 从严格意义上来说,操作系统可被定义为一种软件,它控制计算机硬件资源&…...

IDEA2021.3.1-优化设置IDEA2021.3.1-优化设置、快捷方式改为eclipse、快捷键等
IDEA2021.3.1-优化设置IDEA2021.3.1-优化设置、快捷方式改为eclipse、快捷键等 一、主题设置二、鼠标悬浮提示三、显示方法分隔符四、代码提示忽略大小写五、自动导包六、取消单行显示tabs七、设置字体八、配置类文档注释信息模板九、设置文件编码9.1、所有地方设置为UTF-89.2、…...

使用C#的窗体显示与隐藏动画效果方案 - 开源研究系列文章
今天继续研究C#的WinForm的显示动画效果。 上次我们实现了无边框窗体的显示动画效果(见博文:基于C#的无边框窗体动画效果的完美解决方案 - 开源研究系列文章 ),这次介绍的是未在任务栏托盘中窗体的显示隐藏动画效果的实现代码。 1、 项目目录;…...

09_Vue3中的 toRef 和 toRefs
toRdf 作用:创建一个 ref 对象,其 value 值指向另一个对象中的某个属性。语法: const name toRef(person,name) 应用:要将响应式对象中的某个属性单独提供给外部使用时。扩展:toRef 与 toRefs 功能一致࿰…...

JAVA获取视频音频时长 文件大小 MultipartFileUtil和file转换
java 获取视频时长_java获取视频时长_似夜晓星辰的博客-CSDN博客 <dependency><groupId>ws.schild</groupId><artifactId>jave-all-deps</artifactId><version>2.5.1</version></dependency>Slf4j public class VideoTimeUtil…...

刷题笔记 day9
1658 将 x 减到 0 的最小操作数 解析:1. 当数组的两端的数都大于x时,直接返回 -1。 2. 当数组所有数之和小于 x 时 ,直接返回 -1。 3. 数组中可以将 x 消除为0,那么可以从左边减小为 0 ;可以从右边减小为 0 ࿱…...