解决线程不安全问题的方法

news2025/1/11 14:14:56

解决线程不安全问题:

一、原子性

synchronized关键字的特性(监视锁)

1、synchronized的互斥性

通过特殊手段,让count++变成原子操作

举例:上厕所,人进入后上锁,用完了出来解锁,期间只有自己可以使用这个厕所。

解决线程不安全也是类似的,在count++之前上锁,在count++完之后解锁,在加锁和解锁期间,进行修改,这个期间其他线程想要修改,是修改不了的,只能阻塞等待(线程状态:BLOCKED)。

Java中使用synchronized关键字来加锁

锁的特性:具有独特性,如果当前锁没人来加,加锁操作就成功,如果已经被加上,加锁操作就会阻塞等待。

1.1基础使用:synchronized关键字修饰一个普通方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LYwJPHJO-1677157201827)(https://picgo-char.oss-cn-beijing.aliyuncs.com/202302222130097.png)]

通过这样一个简单的操作,我们就可以保证代码是正确的结果:

image-20230222214709594

image-20230222215753575

本来线程调度,是随机的过程~

一旦两个组load add save交织在一起,就会产生线程安全问题;

现在使用锁,就使这两组load add save能够串行执行

注意:即使t1加锁以后,CPU可能进行调度切换,去执行其他线程,即使t1不在CPU中执行,但是t1仍然是加锁状态,t2依旧是BLOCK状态,无法在CPU上运行。

小思考:

一个线程加锁,一个线程不加锁,这个时候会咋样?线程安全能否保证?

线程安全问题,不是加锁了就一定安全,而是通过加锁,让并发修改同一个变量最后形成的效果是串行修改同一个变量,才是安全的。

加锁的方式,位置不正确,不一定能解决线程安全问题,也可能因为解决了线程安全问题而导致因为线程来回调度的销毁,并发编程的速度不如串行执行。

只给一个线程加锁,没有用!!!加锁的目的是让两个线程可以产生锁竞争!只有一个加了,另外一个不加就不会有锁竞争!

1.2synchronized修饰代码块

如果一个方法中只是有些代码需要加锁,有些不需要,就可以使用这种修饰代码块的方式进行了。

image-20230222221924975

注意:

1、synchronized()这里的括号,里面填的东西,就是你要针对哪个对象加锁(被加锁的对象就叫做锁对象)

2、使用锁的时候,一定要明确,当前是针对哪个对象加锁**(关键)**

3、一个synchronized只能锁一个对象

知道了上面的注意点以后我们再来了解锁对象可以是哪些。

1.2.1锁对象是this

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y0KgTvdG-1677157201830)(https://picgo-char.oss-cn-beijing.aliyuncs.com/202302222223095.png)]

针对当前对象加锁,谁调用了increase方法谁就是this。

image-20230222222545313

1.2.2专门创建Locker对象作为锁对象

任意对象都可以在synchronized里面作为锁对象!!!

我们写多线程代码的时候,不关心这个锁对象是谁,是哪种形态,只关心两个线程是否锁同一个对象,锁同一个对象就有竞争反之亦然。

image-20230222223146811

因为两个线程使用的都是同一份count对象,所以他们count里面的locker对象也是同一份的。

image-20230222223737700


总结:

如果synchronized直接修饰方法,就相当于锁对象是this,大部分情况下,直接写this作为锁对象,一般都是可以的,不行的时候再讨论。

更加广义的锁对象:

Java 中的任何一个对象都可以作为锁对象!!!(成员变量, 局部变量,静态变量,类对象…)

而且这些不同形态的对象,作为锁对象的时候没有任何区别!!!锁对象只是用来控制线程之间的互斥的~~是针对同一个对象加锁,就会出现互斥.是针对不同对象加锁就不会互斥

我们写多线程代码的时候,不关心这个锁对象是谁,是哪种形态,只关心两个线程是否锁同一个对象,锁同一个对象就有竞争反之亦然。

锁对象是类对象时候值得注意:

类对象只有一个

image-20230222225132022

1.3修饰静态方法

image-20230222225645941

修饰静态方法时候相当于锁对象是类对象

2、synchronized的可重入性

整队一个线程,连续对一把锁加锁两次,就可能造成死锁

例如这样的代码:

image-20230222231351555

什么叫做死锁呢?
假设一个线程针对一把锁加锁两次,第一次加锁能够加锁成功,第二次加锁会加锁失败(锁已经被占用)就会在第二次加锁这里阻塞等待,等到第一把锁解锁,但是第一步锁想要解锁则需要执行完第二次加锁里面对应的代码块,也就是要求第二把锁加锁成功才第一把锁才能解锁,这样的死循环就叫做死锁。

针对上诉情况,不会产生死锁的话,这样的锁叫做**“可重入锁”**
针对上诉情况,会产生死锁,这个锁就叫做**“不可重入锁”**

我们学习的synchronized是可重入的。

【八股文】如何实现一个可重入锁?

1、入锁过程

可重入锁底层实现,是很简单的.
只要让锁里面记录好,是哪个线程持有的这把锁

例如t 线程尝试针对 this 来加锁~~ this 这个锁里面就记录了 是 t 线程持有了它;
第二次进行加锁的时候,锁一看,还是 t 线程, 就直接通过了,没有任何负面影响,不会阻塞等待!!!

2、解锁过程:

image-20230222232149693

引入一个计数器~~
每次加锁,计数器 ++
每次解锁,计数器 –
如果计数器为 0,此时的加锁操作才真加锁同样计数器为 0,此时的解锁操作才真解锁

总结:

可重入锁的实现要点:
1、让锁里持有线程对象,记录是谁加了锁

2、维护一个计数器,用来衡量啥时候是真加锁,啥时候是真解锁,啥时候是直接放行

二、两个线程同时修改同一个变量

通过修改代码的顺序来避免两个线程同时修改同一个变量。

三、内存可见性问题

假设,读操作非常频繁:

image-20230222234112094

count2.count == 2 ==> 读内存(LOAD),进行比较(CMP)

这个while循环会循环的非常非常快!频繁进行多次LOAD(读取内存)和CMP(比较寄存器的值是否是2)

在计算机中LOAD消耗的时间比CMP满3-4个数量级,慢太多了

这个时候编译器就开始优化:既然你频繁执行LOAD,并且你的LOAD结果还一样,干脆就执行一次LOAD 就得了.后续进行CMP就不再重新读内存了。

因为在这个t1线程中,没有人修改Count.count的值,编译器就认为读到的结果都是固定的,也就做了一个大胆的决定,只读一次,后面不读了,可以大大提升效率.

但是此时我们加入一个线程2修改整个值,会怎么样?

image-20230222210430398

执行的结果:

image-20230222211433462

运行的时候会出现问题,即使我们t2线程把值修改了,因为编译器优化,t1线程没感知到**(内存可见性问题)**编译器还是认为它没有修改

编译器优化,不应该是保持代码逻辑不变的前提下,才能进行优化嘛??
这里的优化不是让逻辑变了嘛??想让他结束,结束不了,就是bug!!

结论:编译器优化,在多线程环境下可能存在误判!!!

既然编译器自己的判定不准了把不该优化的给优化了,就可以让程序员显式的提醒编译器,这个地方不要优化。

也就是volatile 关键字的作用!!

image-20230222234736756

1、volatile

volatile起到的作用是**“保证内存可见性”**

TIps:volatile不保证原子性

针对的场景:

一个线程读操作,一个线程修改,使用volatile最合适

2、JMM

谈到了volatile,一定少不了JMM(Java Memory Model(java内存模型)

volatile禁止了编译器优化,避免了直接读取CPU寄存器(工作内容/工作存储区/work memory)中缓存的数据,而是每次都重新读内存(主内存/主存储区/main memory)。

主内存才是我们熟悉的内存,工作内存是缓存!

这套说法是Java里面的,目的是为了实现Java跨平台性/通用性。(Java初衷:程序员可以避开硬件的特性,这些特性Java帮我们搞定)

重点:

站在JMM的角度来看待volatile:

正常程序执行的过程中,会把主内存的数据,先加载到工作内存中,再进行计算处理

编译器起到的优化效果就是让程序每次都不是真正的读取主内存的数据,而是直接读取工作内存中的缓存数据(可能导致内存可见性)

volatile起到的效果就是保证程序每次读取内存都是主内存重新读取

四、抢占式执行问题

多线程很讨厌:抢占式执行,调度过程是随机的,很多时候,我们又希望多个线程按照一个预期的顺序来进行执行

wait和notify就是用来调配线程执行的顺序的

1、wait

wait是Object的方法.Object作为Java所有类的祖宗;一次就可以使用任意的类的实例都能调用wait方法。

image-20230223195714693

线程执行到wait,就会发生阻塞;直到另外一个线程,调用notify把这个wait唤醒,才会继续往下走。

我们使用上面的尝试启动t1线程,会发现报错:

image-20230223010051726

解释一下这个报错是什么意思:

image-20230223194356752

使用wait前必须要先加锁!!!

image-20230223195739963

此时程序运行正常~

image-20230223194706976

使用wait时候,本质上做了三件事:

1、释放当前锁

2、进行等待通知

3、满足一定条件的时候(别人调用notify)被唤醒,然后尝试重新获取锁

等待通知的前提,是要先释放锁!而释放锁的前提,是你得加了锁(加上锁,才能谈释放锁),这件事为什么之前不加锁的时候使用wait会报错。

2、notify

同样的,notify也是要包含在synchronized里面的

image-20230223195902266

线程1没有释放锁的话,线程2也就无法调用到notify(因为阻塞等待)

线程1调用wait,在wait里面就释放锁了,这个时候虽然线程1代码在阻塞状态,但是此时锁还是释放的状态,线程2就才能拿到锁。

注意点:

其他线程想要调用notify,就必须得先上锁,调了notify就会唤醒wait,调用wait的线程就会尝试重新获取到锁,但是notify所在线程也得先释放锁,调用wait的线程才能重新获取成功到锁。

不好理解?来一个现场图:

image-20230223201356348

image-20230223201801977

注意事项:

1、要保证加锁的对象,和调用wait的对象得是同一个对象;
还要保证,调用wait的对象和调用notify的对象也是同一个对 象。

image-20230223201823965

调用notify也得和调用wait的是同一个对象:

image-20230223201941906

2、如果t1先调用了wait,t2后调用notify,此时notify会唤醒wait

如果t2先执行了notify,t1后执行了wait,此时并不会有什么影响,错过了就错过了(即使没人调用wait,调用notify并不会有异常,副作用)

3、Java中还有一个notifyAll();notifyAll全都唤醒。

注意:即使唤醒了所有的wait,这些wait需要重新竞争锁,重新竞争锁的过程仍然是串行的。

五、指令重排序

可以使用volatile解决,具体介绍在单例模式中。

扩展:Java 标准库中的线程安全类

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.。

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

有一些是线程安全的,使用了锁的机制:

  • Vector (不推荐使用)
  • HashTable (不推荐使用)
  • ConcurrentHashMap
  • StringBuffer

StringBuffer 的核心方法都带有 synchronized .

还有String引用类型,因为它不涉及到修改

原因:

1、String类型里面的char[]数组是private修饰的,并且没有提供修改的方法(根本)

2、char[]数组使用了final修饰,保证char[]引用的对象不能被修改

3、string类被final修饰,不能被继承。

image-20230223204112659

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

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

相关文章

编译原理【运行时环境】—什么是活动记录、 活动记录与汇编代码的关系

系列文章戳这里👇 什么是上下文无关文法、最左推导和最右推导如何判断二义文法及消除文法二义性何时需要消除左递归什么是句柄、什么是自上而下、自下而上分析什么是LL(1)、LR(0)、LR(1)文法、LR分析表LR(0)、SLR(1)、LR(1)、LALR(1)文法之间的关系编译原理第三章习…

专利撰写 为什么要申请专利 申请专利对个人有什么利益关系 专利申请实例 如何申请专利 专利申请办理流程

专利撰写 专利是对发明者或创造者所创造的发明或设计提供一定期限的独占权的法律保护。撰写专利需要考虑到多方面的因素,包括发明或设计的技术性、可行性、独创性、保密性等等。以下是一些关于专利撰写的常见问题和注意事项:专利类型:专利包括…

记一次后端生成Zip文件通过浏览器下载后文件损坏,无法打开,不可预知的末端错误,下载后文件比源文件增大

记一次后端生成Zip文件问题前言问题出现排查一、流没有关好二、写入了空白字节三、没有flush定位环节一、生成二、通过SwaggerUI、PostMan进行下载三、结论解决方法前言 在项目上线前夕,临时添加了个数据导出的接口,需求是导出压缩包,选择了项…

SpringCloud - Feign远程调用

目录 Feign的远程调用 RestTemplate方式调用存在的问题 介绍与初步使用 Feign的自定义配置 Feign运行自定义配置来覆盖默认配置,可以修改的配置如下: 配置Feign日志有两种方式: Feign性能优化 Feign底层的客户端实现: 连…

MATLAB R2020a 与PreScan8.5.0 详细安装教程(图文版)

目录MATLAB安装PreScan安装每文一语MATLAB安装 MATLAB是一款数学软件,用于科学计算、数据分析和可视化等任务。以下是MATLAB的几个优势: 丰富的工具箱:MATLAB拥有多种工具箱,包括信号处理、图像处理、优化、控制系统等&#xff0…

深度学习论文: EdgeYOLO: An Edge-Real-Time Object Detector及其PyTorch实现

深度学习论文: EdgeYOLO: An Edge-Real-Time Object Detector及其PyTorch实现 EdgeYOLO: An Edge-Real-Time Object Detector PDF: https://arxiv.org/pdf/2302.07483.pdf PyTorch代码: https://github.com/shanglianlm0525/CvPytorch PyTorch代码: https://github.com/shangli…

SQL的四种连接-左外连接、右外连接、内连接、全连接

SQL的四种连接-左外连接、右外连接、内连接、全连接 内连接inner join…on… / join…on… 展现出来的是共同的数据 select m.Province,S.Name from member m inner join ShippingArea s on m.Provinces.ShippingAreaID; 相当于:select m.Province,S.Name from m…

Mybatis一对多查询 ,以及会遇到的各种问题解答

Mybatis一对多查询 ,以及会遇到的各种问题解答业务场景实体类,数据库方法1:连表查询,用ResultMap映射方法2:子查询进行映射业务场景 有时候前端需要表格里面嵌套表格的情况,用以展示更加详细的信息&#xf…

前端历史 --- 从HTML静态文件到前后端分离

前端历史 --- 从HTML静态文件到前后端分离1. 静态HTML2. 动态HTML --- 服务器端渲染CGI --- Common Gateway InterfaceservletASP, JSP, PHP服务器端渲染(SSR)3. 前后端分离 --- 客户端渲染JavaScriptAjax --- Asynchronous Javascript And Xml.客户端渲染1. 静态HTML 在上个世…

回溯问题(子集型回溯、组合型回溯、排列型回溯)【零神基础精讲】

来源0x3f:https://space.bilibili.com/206214 回溯分为【子集型回溯】【组合型回溯】【排列型回溯】 文章目录回溯基本概念[17. 电话号码的字母组合](https://leetcode.cn/problems/letter-combinations-of-a-phone-number/)子集型回溯(分割问题也可以看…

按键中断,红外、光电、火焰传感器中断控制LED等并打印信息

需求:按键中断,红外、光电、火焰传感器中断控制LED等并打印信息重写函数部分:void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin) {switch(GPIO_Pin){case GPIO_PIN_9:HAL_GPIO_TogglePin(GPIOE,GPIO_PIN_10);if(HAL_GPIO_ReadPin(GPIO…

浅析高速服务区交互一体机设备管理系统的建设与方向

很多高速公路服务区均缺乏现代化的服务思维、理念和手段,信息系统功能薄弱,服务区的自助服务终端存在功能单一、人机交互体验差、设备维护管理成本高、联动效率低、运营难等问题,这不仅无法支撑服务区的精细化服务和智能化管理需求&#xff0…

【视频】海康摄像头、NVR网络协议简介

1、软硬件整体架构 2、涉及的网络协议 3、协议简介 3.1 海康私有协议 设备发现SADP:进行设备的发现、激活、修改网络参数、忘记密码等; SDK:4200、系统平台的接入前端设备,协议不对外开放,但对外提供接口库; ISAPI:Intelligent Security API(智能安全API),基于HTTP传输…

C/C++每日一练(20230223)

目录 1. 数据合并 2. 回文链表 3. 完美矩形 1. 数据合并 题目描述 将两个从小到大排列的一维数组 (维长分别为 m,n , 其中 m,n≤100) 仍按从小到大的排列顺序合并到一个新的一维数组中,输出新的数组. 输入描述 第 1 行一个正整数 m , 表示第一个要合并的一维…

【数据结构】AVL树

AVL树一、AVL树的概念二、AVL的接口2.1 插入2.2 旋转2.2.1 左单旋2.2.2 右单旋2.2.3 左右双旋2.2.4 右左双旋三、验证四、源码一、AVL树的概念 当我们用普通的搜索树插入数据的时候,如果插入的数据是有序的,那么就退化成了一个链表,搜索效率…

纵然是在产业互联网的时代业已来临的大背景下,人们对于它的认识依然是短浅的

纵然是在产业互联网的时代业已来临的大背景下,人们对于它的认识依然是短浅的。这样一种认识的最为直接的结果,便是我们看到了各式各样的产业互联网平台的出现。如果一定要找到这些互联网平台的特点的话,以产业端为出发点,无疑是它…

嵌入式:UCOS移植+简单创建任务

目录 一、UCOS操作系统 二、UCOS移植 1、文件介绍 2、UCOS || 源码分析 3、打开Software文件 三、UCOS任务创建 一、UCOS操作系统 C/OS 是 Micrium 公司出品的实时操作系统, C/OS 目前有两个版本: C/OS-II 和 C/OS-III。 C/OS 是一种基于优先级…

Linux GPIO 开发指南

文章目录Linux GPIO 开发指南1 概述1.1 编写目的1.2 适用范围1.3 相关人员2 模块介绍2.1 模块功能介绍2.2 相关术语介绍2.3 总体框架2.4 state/pinmux/pinconfig2.5 源码结构介绍3 模块配置3.1 kernel menuconfig 配置3.2 device tree 源码结构和路径3.2.1 device tree 对 gpio…

Python计算 -- 内附蓝桥题:相乘

计算 ~~不定时更新🎃,上次更新:2023/02/23 🗡常用函数(方法) 1. 求一个整数的最末位 举个栗子🌰 n int(input()) end n % 10蓝桥例题1 - 相乘✖️ 题目描述 本题为填空题,…

MySQL 11:MySQL锁

锁是一种机制,计算机通过这种机制协调多个进程或线程对资源的并发访问(以避免争用)。在数据库中,除了传统的计算资源(如CPU、RAM、I/O等)的争夺外,数据也是一种被众多用户共享的资源。如何保证并…