【多线程】如何解决线程安全问题?

🥰🥰🥰来都来了,不妨点个关注叭!
👉博客主页:欢迎各位大佬!👈

在这里插入图片描述

文章目录

  • 1. synchronized 关键字
    • 1.1 锁是什么
    • 1.2 如何加锁
    • 1.3 synchronized 修饰方法
      • 1) 修饰普通成员方法
      • 2) 修饰静态成员方法
    • 1.4 手动指定一个锁对象
  • 2. volatile 关键字
    • 2.1 volatile关键字保证内存可见性
    • 2.2 volatile关键字不保证原子性
    • 2.3 volatile关键字禁止指令重排序

1. synchronized 关键字

synchronized关键字保证修改操作的原子性

回顾上期代码,发生线程不安全的原因,该代码因为 count++ 操作不是原子性的,而导致bug,那么如何解决呢?
我们把该操作弄成原子的,不就可以了嘛~
通过对这部分代码加锁,形象来说,让它成为一个整体,则可以使其 count++ 操作为原子的,解决上述线程安全问题

 public void add() {count++;}

注意】Java中虽有读写锁,但是一般不会特别去区分,即默认情况下,就是很普通加锁

那么锁是什么呢,如何加呢,我们一起继续来看看

1.1 锁是什么

锁有两个核心操作:加锁和解锁
一旦某个线程加了锁后,其它线程也想加锁,不能直接加上!!! 必须阻塞等待,直到拿到锁的那个线程释放锁为止,即只有拿到锁的线程释放锁,其它线程才可以加锁
在这里插入图片描述

打一个形象的比方,就比如,我们去上厕所,进入厕所后,就将门锁上,这时候其它人无法进入该厕所,等到解决完事情后,打开锁,从厕所出来,则厕所空闲了,其他人想要去上厕所,可以去,但是如果有人正在使用这个厕所,则只能进行阻塞等待!!!
在这里插入图片描述
注意
线程调度是抢占式执行,当里面的人释放锁出来后,等待锁的1号、2号、3号和4号,不是按照顺序执行的,谁能抢先一步拿到锁并成功加锁,是不确定的,每个线程都有概率拿到锁,线程调度是随机的,完全看系统是如何调度~(咱也不知道呀就是说)

1.2 如何加锁

synchronized 是Java中的关键字,直接使用这个关键字来实现加锁的效果
具体使用方法如下:

synchronized (锁对象) { … }

锁的核心操作是加锁和解锁,此处使用代码块的方式来表示:
进入 synchronized 修饰的代码块时,就会触发 加锁
出了 synchronized 修饰的代码块时,就会触发 解锁
补充】{ } 可以想象成一个厕所

在这里插入图片描述
锁对象:表示针对哪个对象加锁 (指明对象)
锁竞争又是什么呢~通过下面这张图,可以生动形象理解:
在这里插入图片描述
锁竞争:一个线程拿到了锁,另一个线程阻塞等待,我们来看看这两种情况
【情况1】如果两个线程针对同一个对象加锁,此时就会出现锁竞争
【情况2】如果两个线程针对不同的对象加锁,此时不会存在锁竞争,各自获得各自的锁即可
在这里插入图片描述
()里面的锁对象,可以写作任意一个Object对象(但内置类型不行!!!)
补充】内置类型即基本数据类型,共八种基本类型~ (快快回顾一下)
在这里锁对象使用的是 this
相当于 Counter counter = new Counter(); 这里的 counter 作为锁对象

回顾之前学的,this指向的是当前对象,通过观察代码,我们不难发现,count++操作,是调用Counter类里面的add()方法,this指向的就是这个当前对象counter
在这里插入图片描述
即锁对象使用 this,哪个对象调用add()方法的就是对哪个对象加锁

在这里插入图片描述

在上述代码中,线程t1,t2 给同一个锁对象,即 this(counter) 加锁,会产生锁竞争,如果 t1 线程拿到锁,那么 t2 线程就会阻塞等待,此时可以保证count++ 自增操作是原子的,不会受多线程抢占式影响!!!
此时再运行程序,则是我们期待的结果——6w,如下图所示:

在这里插入图片描述

再来分析加锁后该组指令的时间图,如下:
在这里插入图片描述

这就可以保证 t2 的 load 一定在 t1 的 save 之后,该 count++ 自增操作就是原子的了,此时计算的结果就是线程安全的!!!
【加锁】本质上,是把并发变成串行~

在上述代码中,一个线程大概做的如下工作:

1)创建 i
2)判定 i < 30000
3) 调用add()方法
4)count++操作
5)add返回
6)i++

在这些步骤中,只有4)count++ 是串行的,其余操作创建i,判定,调用add()方法,add返回,i++等操作两个线程仍然是并行的~ 即加锁是两个线程的某个小部分是串行的,其它大部分仍然是并发滴!

继续拿上厕所为例,A 和 B两个人要做很多事情,但是其中一步,就是要去上同一个厕所,此时这是串行的,得一个个去上,做其它事情就相互不打扰影响啦~人嘛,还是很讲究效率的,但在生活中的某个小小的场景中,还是得遵守规则,一个个来(是吧)

Q:为什么要这样做~ 回到多线程编程的初心,利用多核CPU!!!
A:在保证线程安全的前提下,同时能够让代码跑得更快一些,更好利用多核CPU

在这里插入图片描述
注意
加锁其实是一个较低效的操作,因为加锁就可能涉及到阻塞等待,代码阻塞对于程序的效率肯定还是会有一定的影响的,加锁和解锁也涉及到资源的使用,因此,实际情况中坚持" 非必要,不加锁 "的原则
但是,如果不加锁的话,程序无法执行或执行结果错误出现bug,这是不得不加锁的情况,此时是必须要加锁的!!!

上述代码,是在count++ 操作加锁了,此处虽然加锁了,比不加锁肯定是要慢一点,但仍然比全部串行快,同时也比不加锁算的结果准确,这就是不得不加锁的情况,不加锁的话,结果是不正确的!

1.3 synchronized 修饰方法

1) 修饰普通成员方法

如果直接使用 synchronized 修饰成员方法,相当于以 this 为锁对象
在这里插入图片描述

2) 修饰静态成员方法

如果 synchronized 修饰静态方法,此时不是给 this 加锁了,是给类对象加锁!!!
在这里插入图片描述
解释两个疑惑:
1)static 修饰的方法到底是啥意思?
static修饰的方法,是类方法(静态方法),不依赖对象,即不用创建实例,就可直接用类调用方法
(2)类对象到底是啥呀?
这里的类对象是 Counter.class
.java 源代码文件被 javac 编译为 .class 二进制字节码文件,此时JVM就可以执行.class
在这里插入图片描述
而JVM要想执行这个.class文件,就必须得先把文件内容读取到内存中,这个操作叫做类加载
在内存中,用类对象来表示.class文件内容

.class文件的内容,描述了类的方方面面的详细信息,包括不限于:

1.类的名字
2.类有哪些属性,属性的名字,类型,权限
3.类有哪些方法,方法的名字,参数,类型,权限
4.类继承自哪个类
5.类实现了哪些接口 …

此处的类对象,相当于"对象的图纸",有了这个图纸,才了解这个对象到底是什么样,进一步的可以使用反射API来获取这里的一些信息

注意事项
反射属于非常规手段,Java的反射API,用起来比较麻烦容易出错,不要轻易使用反射,除非实在没有办法啦!

1.4 手动指定一个锁对象

更常见的还是手动指定一个锁对象!
synchronized () 括号中的锁对象,可以写作任意一个Object对象 (但内置类型不行!!!)
在前面提到过,这里演示一下自己手动指定一个锁对象,如下:

    private Object locker = new Object();public void add() {synchronized (locker) {count++;}}

在这里插入图片描述
实际上,锁对象只是一个吉祥物,没啥特别的,唯一的作用依旧是:如果多个线程尝试针对同一个锁对象加锁,此时会产生锁竞争,针对不同对象加锁,就不会有锁竞争,各自忙活~
我们需要牢记这句话!!!

2. volatile 关键字

volatile关键字保证内存可见性和禁止指令重排序

2.1 volatile关键字保证内存可见性

继续回顾一下上期的内容,另一个线程不安全的场景,由于内存可见性,引起的线程不安全~
接下来先写一个bug出来,我们对此进行具体的分析:

public class ThreadTest1 {public static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(()->{while(flag == 0) {//不写任何东西}System.out.println("while循环结束,t1结束");});Thread t2 = new Thread(()->{Scanner sc = new Scanner(System.in);System.out.println("请输入一个整数:");flag = sc.nextInt();});t1.start();t2.start();}
}

预期效果
t1 线程通过 flag == 0 作为条件进行循环初始情况,将进入循环
t2 线程通过控制台输入一个整数,一旦用户输入了非 0 的值,此时 t1 的循环就会立即结束,从而 t1 线程退出
实际效果
输入非 0 的值之后,t1 线程并没有退出!!!循环未结束,实际效果如下:

在这里插入图片描述
预期效果与实际效果不一致,很显然出现了bug!!!

通过 jconsole 工具可以清楚看到 t1 线程仍然在执行,处于RUNNABLE状态,如下图所示:

在这里插入图片描述

为什么会出现这种情况捏!!! 都是内存可见性的锅~

解释说明
while 循环中,flag == 0 进行两步的操作:

1)load 从内存读取数据到CPU寄存器
2)cmp 比较寄存器里面的值是否为 0

此处这两个操作,load 的时间开销远远高于 cmp!!!
读内存虽然比读硬盘快很多,但是读寄存器比读内存快几千倍!!!

编译器就发现了:
1)load 的开销很大
2)每次 load 的结果都一样

这个时候编译器就做了一个很大胆的操作,把 load 操作优化掉,只有第一次执行 load 才真正执行了,后续循环都只是进行 cmp 比较寄存器的值是否为 0 的操作,相当于直接复用之前寄存器中 load 的值,即 0,所以循环会一直执行下去,t1 线程不会结束

补充知识——编译器优化
编译器优化,是一个十分普遍的事情,编译器优化就是能够智能调整代码的执行逻辑,“保证程序结果不变的前提下”,通过加减语句、语句变化等,通过一些类的操作,使整个程序执行的效率大大提升

但是编译器对于 “程序结果不变” 单线程下判定是非常准确的,但是多线程就不一定了,编译器可能出现误判,可能调整之后,效率变高,但是结果变了,引起bug!!!

解决办法在变量前加 volatile 关键字,保证内存可见性~ 此时编译器会禁止上述的编译器优化,就是被volatile 修饰的变量能够保证每次都是从内存重新读取数据

仅需修改这一行代码,在 flag 变量前加 volatile 关键字修饰

volatile public static int flag = 0;

再次运行程序,我们可以看到:
在这里插入图片描述
这里就符合我们的预期了,加上 volatile 关键字之后,此时编译器能够保证每次都是重新从内存读取,flag的变量的值,此时 t2 线程修改 flag,t1 线程就可以立即感受到了,t1 线程就可以正确退出
在这里插入图片描述

直接访问工作内存(上期内容介绍到过,工作内存是寄存器或者CPU的缓存),速度会变得非常快,但是可能出现数据不一致的情况,导致出现bug,加上volatile修饰后,必须重新从内存读,数据则准确~
这也说明了程序的效率和准确性往往不可以兼得!!!(做人不要太贪心~)

总得来说,变量被 volatile 关键字修饰,可以保证内存可见性,保证每次都是重新从内存读取~

2.2 volatile关键字不保证原子性

volatile 关键字与synchronized 关键字两者有着本质区别,synchronized 关键字保证原子性,volatile 关键字不保证原子性!!!(敲黑板~强调啦)
volatile 关键字不保证原子性,保证的是内存可见性!与原子性无关~
(synchronized 关键字是否也能保证内存可见性,是存在很多争议的,这里不给出答案~)
总结

1.volatile 关键字适用的场景:是一个线程读,一个线程写的情况
2.synchronized 关键字适用的场景:多个线程写的情况

2.3 volatile关键字禁止指令重排序

volatile 关键字还有一个效果,就是禁止指令重排序~

继续再回顾一下上期线程安全内容,引起线程安全的原因中,有一个是因为编译器优化造成指令重排序而导致的问题

指令重排序也是编译器优化的策略,调整代码的执行顺序,让程序更高效,也是"保证整体的逻辑不变"

再举一个栗子,深入了解指令重排序:
假如我要去超市买零食,我可以这样买:
1)西瓜
2)薯片
3)糖果
4)饮料
但是这样多走了很多路,效率低,如果我调整购买顺序,按照4) —> 2) —> 3)—> 1)这样的顺序购买,更为高效,并且我想要买的东西也都买到了!
在这里插入图片描述同样,谈到优化,保证整体的逻辑不变,在单线程下很容易,但是在多线程下,很容易出错,导致bug

指令重排序这个问题,很难用代码演示,因为大部分下情况是正确的,所以这里进行模拟演示:
在这里插入图片描述
大体可以分为以下三步:
1)申请内存空间
2)调用构造方法,即初始化内存的数据
3)把对象引用赋值给 s,即内存地址的赋值

假设在这种情况下:
t1 线程按照1)—> 3) —> 2) 的顺序执行,当线程 t1 执行完 1)和 3)后,即将执行线程 t2 的时候,t2 开始执行
由于 t1 的 3)已经执行过了,这个引用 s 已经非空了,t2 线程中的 if 条件成立,t2 就尝试调用s.learn()方法,但是 t1 线程还是一个毛坯房,没有进行装修,即没有进行初始化,此时调用的 learn() 方法会变成什么样,无从知晓,所以很可能产生bug!!!

但是如果是单线程环境下,此处可以进行指令重排序,1)肯定先执行,2)和 3)谁先执行,谁后执行,都是可以的,没有什么影响

解决方法
这个场景使用 volatile 关键字修饰 s,创建对象的时候就会禁止指令重排序! (能够保证先装修再拿到钥匙)
当然,这个也可以用加锁的方式解决,用synchronized 关键字修饰,如下:

private Object locker = new Object();synchronized(locker) {s = new Studnet();
}synchronized(locker) {if(s != null) {s.learn();}
}

通过这期内容加深引起线程安全的原因理解,并知道如何进行解决~(收获满满呀!)

💛💛💛本期内容回顾💛💛💛
在这里插入图片描述✨✨✨本期内容到此结束啦~

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

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

相关文章

【系统架构设计师】七、信息安全技术基础知识(访问控制技术|抗攻击技术|计算机系统安全保护能力等级)

目录 一、访问控制技术 二、信息安全的抗攻击技术 2.1 分布式拒绝服务DDoS与防御 2.3 ARP欺骗攻击与防御 2.4 DNS欺骗与防御 2.5 IP欺骗与防御 2.6 端口扫描&#xff08;Port Scanning&#xff09; 2.7 强化TCP/IP堆栈以抵御拒绝服务攻击 2.8 系统漏洞扫描 三、信息安…

基于weixin小程序乡村旅游系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;商家管理&#xff0c;旅游景点管理&#xff0c;景点类型管理&#xff0c;景点路线管理&#xff0c;系统管理 商家帐号账号功能包括&#xff1a;系统首页&#xff0c;旅游景点管理&…

解决RuntimeError: Unsupported image type, must be 8bit gray or RGB image.

今天在使用Opencv进行人脸识别项目时发现了一个问题&#xff0c;一直报这个错误RuntimeError: Unsupported image type, must be 8bit gray or RGB image.查了一下资料也是解决了&#xff0c;这样给大家分享一下 解决方案 Numpy 有一个主要版本更新&#xff0c;与 dlib 不兼容。…

【Docker】创建 swarm 集群

目录 1. 更改防火墙设置 2. 安装 Docker 组件 3. 启动 Docker 服务&#xff0c;并检查服务状态。 4. 修改配置文件&#xff0c;监听同一端口号。 5. 下载 Swarm 组件 6. 创建集群&#xff0c;加入节点 7. 启动集群 8. 查询集群节点信息 9. 查询集群具体信息 10. 查询…

电脑文件concrt140.dll丢失要怎么恢复?靠谱修复方法分析

电脑文件concrt140.dll丢失这种情况&#xff0c;相对来说还是比较少见的&#xff01;但是不代表没有&#xff0c;既然有人出现这种情况了&#xff0c;那么小编势必要给大家详细的讲解一下concrt140.dll这个文件&#xff0c;以及我们要怎么去解决concrt140.dll文件丢失的问题。下…

hnust 1817 算法10-10,10-11:堆排序

hnust 1817 算法10-10,10-11&#xff1a;堆排序 题目描述 堆排序是一种利用堆结构进行排序的方法&#xff0c;它只需要一个记录大小的辅助空间&#xff0c;每个待排序的记录仅需要占用一个存储空间。 首先建立小根堆或大根堆&#xff0c;然后通过利用堆的性质即堆顶的元素是最…

pppd 返回错误码 含义

错误码 00&#xff1a; pppd已经断开&#xff0c;或者已经成功建立连接后请求方又中 断了。 01&#xff1a; 发成了一个严重错误&#xff0c;例如系统调用失败或者访问非法内存。 02&#xff1a; 处理给定操作是检测到错误&#xff0c;例如使用两个互斥的操作。 03&#xff1a;…

如何获取Power BI的个性可视化控件?

我们在使用Power BI Desktop自带可视化控件进行报表设计的时候&#xff0c;有的时候会发现自带控件使用起来略显单薄&#xff0c;需要一些更有创意或者更能直接吸人眼球的可视化控件。 那有没有地方可以让我们找到一些个性化控件呢&#xff1f; 答案是肯定的&#xff0c;目前P…

vscode 安装Vue插件

打开扩展面板 --> 点击左侧的扩展图标&#xff0c;或者按下快捷键 Ctrl Shift X 搜索插件,在搜索框中输入 Vue vue-helper 用来快捷提示&#xff0c;如果使用elementui的话&#xff0c;插件不会自动提示&#xff0c;安装了它&#xff0c;组件、属性都会有提示了 Vetur V…

嵌入式Linux系统编程 — 4.1 字符串输入输出

目录 1 字符串输出 1.1 字符串输出函数简介 1.2 示例程序 2 字符串输入 2.1 字符串输入简介 2.2 示例程序 程序运行时&#xff0c;需打印信息至标准输出 stdout 设备 或标准错误 stderr设备&#xff08;譬如屏幕&#xff09;&#xff0c;如调试信息、报错信息、中间产生的…

Java | Leetcode Java题解之第202题快乐数

题目&#xff1a; 题解&#xff1a; class Solution {private static Set<Integer> cycleMembers new HashSet<>(Arrays.asList(4, 16, 37, 58, 89, 145, 42, 20));public int getNext(int n) {int totalSum 0;while (n > 0) {int d n % 10;n n / 10;totalS…

枫清科技创始人高雪峰:不取侥幸之利,做难而正确的事!丨数据猿专访

大数据产业创新服务媒体 ——聚焦数据 改变商业 金庸有一本著作叫做《侠客行》&#xff0c;这部武侠小说的主角叫做石破天&#xff0c;他从小的时候便跟随少林弟子习武。长大后&#xff0c;随着自己获得的感悟越来越多&#xff0c;最终选择开宗立派&#xff0c;独创一门武功行…

碧海威L7云路由无线运营版 confirm.php/jumper.php 命令注入漏洞复现(XVE-2024-15716)

0x01 产品简介 碧海威L7网络设备是 北京智慧云巅科技有限公司下的产品,基于国产化ARM硬件平台,采用软硬一体协同设计方案,释放出产品最大效能,具有高性能,高扩展,产品性能强劲,具备万兆吞吐能力,支持上万用户同时在线等高性能。其采用简单清晰的可视化WEB管理界面,支持…

【ONLYOFFICE 8.1】的安装与使用——功能全面的 PDF 编辑器、幻灯片版式、优化电子表格的协作

&#x1f525; 个人主页&#xff1a;空白诗 文章目录 一、引言二、ONLYOFFICE 简介三、安装1. Windows/Mac 安装2. 文档开发者版安装安装前准备使用 Docker 安装使用 Linux 发行版安装配置 ONLYOFFICE 文档开发者版集成和开发 四、使用1. 功能全面的 PDF 编辑器PDF 查看和导航P…

交易例子----qmt实盘分钟交易例子,提供交易源代码

今天给大家一个利用qmt_trader交易策略&#xff0c;我现在实盘使用的系统是自己开发的&#xff0c;只需要把qmt_trader当中第三方库使用就可以&#xff0c;源代码开源开源直接下载 量化系统--开源强大的qmt交易系统&#xff0c;提供源代码 参考教程使用&#xff0c;下载当第三…

ONLYOFFICE桌面编辑器8.1版:个性化编辑和功能强化的全面升级

ONLYOFFICE是一款全面的办公套件&#xff0c;由Ascensio System SIA开发。该软件提供了一系列与微软Office系列产品相似的办公工具&#xff0c;包括处理文档&#xff08;ONLYOFFICE Document Editor&#xff09;、电子表格&#xff08;ONLYOFFICE Spreadsheet Editor&#xff0…

Ubuntu Nvidia GPU驱动安装和故障排除

去官网 菜单列表下载&#xff0c;或者直接下载驱动 wget https://cn.download.nvidia.com/XFree86/Linux-x86_64/550.54.14/NVIDIA-Linux-x86_64-550.54.14.run 安装驱动 /data/install/NVIDIA-Linux-x86_64-550.54.14.run 执行命令&#xff0c;显示GPU情况 出错处理&…

【深度学习】tensorboard的使用

目前正在写一个训练框架&#xff0c;需要有以下几个功能&#xff1a; 1.保存模型 2.断点继续训练 3.加载模型 4.tensorboard 查询训练记录的功能 命令&#xff1a; tensorboard --logdirruns --host192.168.112.5 效果&#xff1a; import torch import torch.nn as nn impor…

视频网站系统

摘 要 随着互联网的快速发展和人们对视频内容的需求增加&#xff0c;视频网站成为了人们获取信息和娱乐的重要平台。本论文基于SpringBoot框架&#xff0c;设计与实现了一个视频网站系统。首先&#xff0c;通过对国内外视频网站发展现状的调研&#xff0c;分析了视频网站的背景…

一站式uniapp优质源码项目模版交易平台的崛起与影响

一、引言 随着信息技术的飞速发展&#xff0c;软件源码已成为推动行业进步的重要力量。源码的获取、交易和流通&#xff0c;对于开发者、企业以及项目团队而言&#xff0c;具有极其重要的意义。为满足市场对高质量源码资源的迫切需求&#xff0c;一站式uniapp优质源码项目模版…