javaee初阶———多线程(三)

在这里插入图片描述

T04BF

👋专栏: 算法|JAVA|MySQL|C语言

🫵 小比特 大梦想

此篇文章与大家分享多线程专题第三篇,关于线程安全方面的内容
如果有不足的或者错误的请您指出!

目录

  • 八、线程安全问题(重点)
    • 1.一个典型的线程不安全的例子
    • 2.出现线程不安全的原因
    • 3.解决线程不安全的问题
      • 3.1针对原因1(线程在系统中的执行是随机的)
      • 3.1针对原因2(存在多个线程同时修改一个变量)
      • 3.2针对原因3(线程针对变量的操作不是"原子"的)
        • 创建出一个对象,用这个对象作为锁
        • 使用synchronized
        • 执行过程
        • 另外几种操作
        • 死锁
          • 死锁实际上有三种比较典型的场景:
          • 避免死锁问题
      • 3.3针对原因4(内存可见性引起的线程安全问题)
        • volatile关键字
  • <center><font color=red>感谢您的访问!!期待您的关注!!!<br>

八、线程安全问题(重点)

我们在前面说过,线程之间是抢占式执行的,这样产生的随机性,使得程序的执行顺序变得不一致,就会使得程序产生不同的结果,有的时候这些不同的结果,我们是不可接受的,认为是一种bug
那么由多线程引起的bug,这样的问题就是线程安全问题,存在线程安全问题的代码,就认为线程是不安全的

1.一个典型的线程不安全的例子

在这里插入图片描述
我们在编写程序的时候的预期值是10000,但是得到的结果确实不确定的,小于10000的
这就是一个典型的多线程并发导致的问题
实际上我们的count++这一步操作包含了3步
(1)load : 将内存中count 的值读取到寄存器里面
(2)add:把寄存器里的count 进行+1操作,后还是保存到寄存器里面
(3)save:将寄存器里的值写回到内存里面
那么由于抢占式执行,在两个线程执行的过程中就有可能出现下面这种情况:
在这里插入图片描述
在上面的执行过程中,我们发现,两个线程分别执行了一次count++,但是由于前一个写会到内存的count被后一个写回去的给覆盖了,最后内存的count还是1
由于当前线程里的执行顺序是不确定的,.有的时候顺序加两次,结果就是对的,有的时候加两次,结果只是加了一次,具体有多少次,结果是多少,都是随机的
因此看到的结果不是一个确定的数,是不可预测的
如果我们能够保证,一个线程的save是在另一个线程的load之前,那么结果就是我们所预期的

2.出现线程不安全的原因

(1)线程在系统中的执行是随机的,抢占式执行,这也是线程安全问题的罪魁祸首
(2)当前代码,存在多个线程同时修改一个变量
如果是一个/多个线程修改单独的变量,那就没事;如果是多个线程读取同一个变量,也没事
但是如果是多个线程同时修改同一个变量,那就有问题了
(3)线程针对变量的操作不是"原子"的
有的操作,例如对int 类型的变量进行赋值操作,在cpu就是一个move指令,但是有的修改操作不是一个原子的,像count++这种
(4)内存可见性问题引起的线程不安全
(5)指令重排序问题引起的不安全

3.解决线程不安全的问题

解决问题就要从产生问题的原因入手

3.1针对原因1(线程在系统中的执行是随机的)

这种我们就无能为力了,我们无法干预

3.1针对原因2(存在多个线程同时修改一个变量)

虽然是一个切入点,但是实际上这种做法不是很普适,只是针对一些特定的场景可以使用,例如String就是个不可变的变量

3.2针对原因3(线程针对变量的操作不是"原子"的)

针对原因三是我们解决线程安全问题最普适的方案,可以通过一些操作让原来不是原子的操作.打包成一个原子的操作,这个操作就是"加锁"
锁实际上也是操作系统提供的功能,也就是内核提供的功能,通过系统api给应用程序,而jvm中又对这样的api进行了封装.方便我们使用
关于锁,主要就是两方面的操作
(1)加锁:t1线程加锁后,t2如果尝试加锁,就会进入阻塞等待(都是操作系统内核在控制),此时就能看到t2线程处于blocked状态
(2)解锁,直到t1解锁后,t2才有机会拿到锁(加锁成功)
这种就是锁的"互斥特性",即锁竞争 / 锁冲突
那么怎么使用锁呢??

创建出一个对象,用这个对象作为锁
Object o = new Object();//在java中,随便拿一个对象都能作为加锁的对象(这个是java中特例的设定)
使用synchronized
public class test1 {private static int count = 0;public static void main(String[] args) {Object locker = new Object();Thread t = new Thread(()->{for(int i = 0; i < 50000; i++){synchronized (locker){count++;}}});t.start();}
}

(1)synchronized是java的关键字(不是方法)
(2)synchronized后面带上的(),()里面带的对象就是"锁对象"

注意:锁对象的用途只有一个,就是用来区分两个线程是否是针对同一个对象加锁,如果是,就会出现"锁竞争/锁冲突",就会出现阻塞等待
而至于接下来这个对象是否使用,有什么方法,带有什么属性,统统都不关心

(3)synchronized()后面带着的{ }
表示当进入这个代码块,就是给上述( )锁对象进行了加锁操作
当出了这个代码块,就是给上述的锁对象解锁

在这里插入图片描述
如图所示,此时这个代码就是两个线程针对同一个对象进行加锁,就会出现互斥现象,那么此时的结果就是我们所预期的了

执行过程

在这里插入图片描述
当t1先加锁后,t2尝试进行加锁,就会进入阻塞状态,当t1的save执行完后,才会释放锁,那么就能够保证一个线程的save在另一个线程的load之前了,即强行构造出"串行执行"的效果

注意:这里的两个线程中,只是针对count++这个操作是串行执行的,但是执行for循环之间的条件判断 / i++ 等操作,还是并发执行的
此时大部分代码还是并发执行的,线程任然可以认为是并发的

另外几种操作

将锁加在for循环外面
在这里插入图片描述
此时for循环就不能并发了

在对象里面的方法里加锁
在这里插入图片描述
由于方法里面只是count++,此时锁的生命周期和方法的生命周期实际上是一致的
那么我们就可以直接将锁加在方法上
在这里插入图片描述
这种写法就相当于一进入方法就进行加锁操作

注意:这种写法不是没有this(锁对象).而是省略了

那如果是static方法呢??
在这里插入图片描述
由于static方法是不依赖对象的,那么此时就相当于针对该类的类对象进行加锁
即等效于:

   public void func(){synchronized (Counter.class){//...}}

这里的Coout.class就是所谓的类对象

关于类对象,最初我们类,都是在.java源代码里面提供的,经过javac编译后,形成.class字节码文件,此时上述的类信息依然存在,但是是二进制形式了
而java运行.class字节码文件,就会读取这里的内容加载到类内存中,给后续使用这个类提供基础,所以jvm在内存里面保存上述信息的对象,就是类对象
但是此时一旦有多个线程不同的Counter对象调用func,都会触发锁竞争
但是使用我们上面使用this就不一定了,因为this可能指向不同的对象

死锁
class Counter {private static int count = 0;public void add(){synchronized(this){count++;}}public static int getCount() {return count;}
}
public class Demo1 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(() -> {for(int i = 0; i < 50000; i++){synchronized (counter){counter.add();}}});t1.start();t1.join();System.out.println(Counter.getCount());}
}

就类似上面的代码,我们在进入for循环的时候,肯定就先获得了锁,接着进入add方法中又尝试针对同一个锁对象进行加锁,但是按照我们上面的说法来说,就会进入阻塞等待,直到第一把锁被释放,但第一把锁被释放,又要先等第二把锁获得,这样就进入了矛盾了

这种情况,就是"死锁"
那么按照我们上面的逻辑来说,就会卡死
在这里插入图片描述
但是居然可以运行通过???
实际上我们上述分析的过程针对java里面的synchronized是不适用的,在C++或者Python是适用的
是因为在synchronized内部做了特殊处理,在每一个锁对象里面都会记录当前是哪个线程持有了这个锁,当某个多线程里面要进行加锁时,就会进行判断,判断当前进行加锁的线程是否已经持有了该锁??
如果没有,那么就阻塞等待,如果有,那么就放行
那么此时实际上,内层的锁就没有什么用了,因为外层的锁就已经保证了线程安全了.而之所以要搞这一套,就是要防止程序员粗心大意搞出死锁

死锁实际上有三种比较典型的场景:

场景1:锁是不可重入锁,并且同一个线程针对同一个对象重复加锁两次就会造成死锁(java中synchronized不会出现这种问题)

场景2:两个线程两把锁
存在锁1和锁2,以及线程1和线程2,线程1拿到锁1后,在锁里面尝试去拿锁2;但是此时锁2已经被线程2拿走了,而恰恰线程2又尝试去拿锁1

    public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){//...try {Thread.sleep(1000);//让线程2拿到锁} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){//...}}});Thread t2 = new Thread(() -> {synchronized (locker2) {//...synchronized (locker1) {//...}}});t1.start();t2.start();}

在这里插入图片描述
此时进程就会僵住,卡死了
因为此时t1等待t2释放锁2,才能释放锁1,但是t2释放锁2之前要等待t1释放锁1

场景3:N个线程 N 把锁
一个典型的例子就是哲学家就餐问题
在这里插入图片描述
每个哲学家都坐在两个筷子之间,每个哲学家啥时候吃面条,啥时候思考人生都是不确定的(抢占式执行)

这么模型在大部分情况下是没问题的,可以最后正常工作的

但是如果出现极端情况,就会出现问题

即在同一时刻,所有的哲学家都拿起左边的筷子,此时就会出现所有的哲学家都无法拿起右边的筷子的情况,但是哲学家又是比较固执,不吃到面条就永远不会放下筷子
这就是典型的死锁状态

避免死锁问题

避免死锁问题,我们就要先理解产生死锁的必要条件
(1)锁具有互斥性:这是synchronized的基本特性
(2)锁不可被抢占(不可被剥夺):即一个线程拿到锁后,除非他自己释放,否则别的线程是拿不到锁的(也是锁的基本特性)
(3)请求和保持 : 一个线程拿到一个锁后,不释放这个锁的前提下,又尝试去获取别的锁(代码层面的特性)
(4)循环等待 多个线程获取多个锁的过程中,出现了A等待B,B又等待A的情况(代码层面的特性)
"必要条件"说明缺一不可
那么我们要避免死锁就只要避免其中一个就好了
对于前两点.由于是锁的基本特性,除非你自己实现锁.实现可以打破互斥,打破不可剥夺这样的条件,对于synchronized这样的锁是不行的

那么我们就可以从(3)(4)代码结构入手

第一点就是不要让锁嵌套获取
在这里插入图片描述
但是有的时候,针对某些场景,就必须嵌套获取了

在这里插入图片描述
上述代码,就变成了t1执行完所有逻辑后释放locker1之后,才轮到t2执行,自然就不会死锁了
有时候在代码中确实需要用到多个线程获取同一把锁,约定好加锁的顺序,就可以有效避免死锁了
这是一个最简单有效的方法

3.3针对原因4(内存可见性引起的线程安全问题)

public class Test6 {public static int count = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (count == 0){//}System.out.println("t1结束");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("输入一个整数");count = scanner.nextInt();});t1.start();t2.start();}
}

此时按照我们的需求,输入任意一个数后,由于count改变了,那么t1里面循环就会结束,t1线程就会退出
但是实际上:
在这里插入图片描述
t1线程并没有结束
而当我们在循环里面尝试进行一些输出操作:
在这里插入图片描述
此时就没问题了

我们来分析一下出现这种问题的原因:
实际上while(count == 0)这个操作的执行流程
(1)load 从内存里面读count到cpu寄存器
(2)cmp 条件成立的话就执行流程,不成立就跳转到别的地方执行
实际上,由于while循环执行得太快,短时间内出现大量的load和cmp操作,而执行load操作消耗的时间,比cmp操作要多上几千倍上万倍
此时jvm发现,在t2修改count之前,每次执行load操作的结果实际上都是一样的,这时候jvm干脆就把load操作给优化掉了(把速度慢的优化掉,使程序运行速度更快了),这样的话,只是第一次真正的load,后续的load都是只是读取之前保留在寄存器里面的值
而出现优化后,t2线程修改count,t1就感知不到了

而当我们上述代码循环体中存在IO操作或者阻塞操作(sleep),这时候就会使循环的旋转速度大幅度降低了,此时的IO操作才是应该被优化的那一个,但是IO操作是不能被优化掉的,load被优化的前提是反复load的结果是相同的.而IO操作注定是反复执行结果是不相同的

致命的是,编译器到底啥时候优化是个"玄学问题"
但是就"内存可见性"问题来说,可以通过特殊的手段来控制,不让他触发优化的 ,那就是使用volatile关键字

volatile关键字

给变量修饰加上这个关键字后,此时编译器就知道,这个变量是"反复无常的",就不能按照上述优化策略进行优化了
(具体在java中,是让javac生成字节码的时候产生了"内存屏障"相关的指令)
但是这个操作和之前的synchronized保证的原子性是没有任何关系的
volatile是专门针对内存可见性的场景来解决问题的,并不能解决循环count++的问题

但是实际上使用synchronized也能一定程度解决问题,此时和代码里面的Io操作是等价的,相比于Load操作来说加锁开销更大,自然就不会对load进行优化了

在这里插入图片描述

关于指令重排序引起的线程不安全问题,在下一篇文章会与大家分享!!!

感谢您的访问!!期待您的关注!!!

在这里插入图片描述

T04BF

🫵 小比特 大梦想

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

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

相关文章

进程等待waitwaitpid

文章目录 进程等待进程等待的必要性进程等待的方法waitwaitpidstatus 非阻塞等待 进程等待 任何子进程&#xff0c;在退出的情况下&#xff0c;一般必须要被父进程等待 进程等待的必要性 1.父进程通过等待&#xff0c;解决子进程退出的僵尸问题&#xff0c;回收系统资源。 2.…

深入浅出 -- 系统架构之日均亿级吞吐量的网关架构(DNS轮询解析)

在前篇关于《Nginx》的文章中曾经提到&#xff1a;单节点的Nginx在经过调优后&#xff0c;可承载5W左右的并发量&#xff0c;同时为确保Nginx的高可用&#xff0c;在文中也结合了Keepalived对其实现了程序宕机重启、主机下线从机顶替等功能。 但就算实现了高可用的Nginx依旧存在…

ccf201509-3模板生成系统(list,map,字符串综合运用)

问题描述 成成最近在搭建一个网站&#xff0c;其中一些页面的部分内容来自数据库中不同的数据记录&#xff0c;但是页面的基本结构是相同的。例如&#xff0c;对于展示用户信息的页面&#xff0c;当用户为 Tom 时&#xff0c;网页的源代码是&#xff1a; 而当用户为 Jerry 时…

Ubuntu 安装Java、Git、maven、Jenkins等持续集成环境

Ubuntu 持续集成 安装OpenJdk 查看所有可安装的 JDK 版本 apt list OpenJDK\*使用 apt 安装 JDK&#xff08;以 11为例&#xff09;,最好是用11&#xff0c;java8对应的jenkins会有兼容问题。 sudo apt install openjdk-11-jdk openjdk-11-jre安装成功后&#xff0c;可以使用以…

2011年认证杯SPSSPRO杯数学建模A题(第二阶段)客机水面迫降时的姿态全过程文档及程序

2011年认证杯SPSSPRO杯数学建模 A题 客机水面迫降时的姿态 原题再现&#xff1a; 2009 年 1 月 15 日下午&#xff08;美国东部时间&#xff09;&#xff0c;US Airways 所属第 1549 航班&#xff08;空中客车 A320 客机&#xff09;在起飞后不久在纽约哈德逊河紧急迫降。经及…

《战神4》和《战神5》有什么联系吗 苹果电脑如何运行《战神4》苹果电脑玩战神 Mac玩游戏 战神5攻略 crossover激活码

《战神4》&#xff08;God of War 2018&#xff09;和《战神5》&#xff08;God of War: Ragnark&#xff09;是一对引人注目的游戏作品&#xff0c;它们不仅在游戏界引起了广泛的关注&#xff0c;也给玩家带来了深入探索北欧神话世界的机会。这两部游戏之间的联系不仅体现在剧…

UE5 在骨骼动画模型上绘制贴图

参考&#xff1a;Unreal 5.1 - How to paint damage textures and other effects on skeletal meshes 针对模型&#xff0c;在运行状态下通过射线指定一定范围&#xff0c;添加材质效果。 核心思路 通过射线获取命中点&#xff0c;作为材质参数材质中&#xff0c;命中的世界…

缺失msvcr110.dll要怎么处理?快捷的修复msvcr110.dll方法

当你在使用电脑进行工作或娱乐时&#xff0c;可能会突然遇到一个错误提示&#xff1a;“程序无法启动&#xff0c;因为电脑中缺失msvcr110.dll”。这样的情况不仅会打断你的活动&#xff0c;还可能带来一定程度的不便。面对这个在Windows操作系统中相对常见的问题&#xff0c;其…

JavaEE实验三:3.5学生信息查询系统(动态Sql)

题目要求: 使用动态SQL进行条件查询、更新以及复杂查询操作。本实验要求利用本章所学知识完成一个学生信息系统&#xff0c;该系统要求实现3个以下功能: 1、多条件查询&#xff1a; 当用户输入的学生姓名不为空&#xff0c;则根据学生姓名进行学生信息的查询&#xff1b; 当用户…

将Ubuntu18.04默认的python3.6升级到python3.8

1、查看现有的 python3 版本 python3 --version 2、安装 python3.8 sudo apt install python3.8 3、将 python3.6 和 3.8 添加到 update-alternatives sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.6 1 sudo update-alternatives --insta…

开发日志2024-04-11

开发日志2024/04/11 1、会员/普通用户预约完成后&#xff0c;技师对应的积分添加预约完成的项目价格添加到统计表的业绩字段中&#xff0c;同时对应的服务次数字段1 实现代码&#xff1a; 前端 shHandler(){this.$confirm(确定操作?, "提示", {confirmButtonText…

flutter跑通腾讯云直播Demo

运行示例 前提条件 要求java jdk 11版本 并且配置到了环境变量 重要 要求flutter 版本 2.8.0 并且配置到了环境变量 重要 要求dart-sdk版本2.15 并且配置到了环境变量 重要 您已 注册腾讯云 账号&#xff0c;并完成 实名认证。 申请 SDKAPPID 和 SECRETKEY 登录实时音视频控…