【JavaEE】多线程安全问题

devtools/2024/10/21 4:00:20/

文章目录

  • 1、什么是多线程安全问题
  • 2、出现线程不安全的原因
    • 2.1 线程在系统中是随机调度,抢占式执行的
    • 2.2 多个线程同时修改同一个变量
    • 2.3 线程针对变量的修改操作,不是“原子”的
    • 2.4 内存可见性问题
    • 2.5 指令重排序
  • 3 、如何解决线程安全问题
    • 3.1 锁操作
    • 3.2 synchronized关键字
  • 4、不正确加锁引发的问题
    • 4.1 一个加锁一个不加锁
    • 4.2 可重入锁
    • 4.3 两个线程两把锁-死锁
    • 4.4 死锁的四个特性
      • 5.1 互斥特性
      • 5.2 不可抢占(不可被剥夺)
      • 5.3 请求和保持
      • 5.4 循环等待
    • 4.5 如何避免死锁


1、什么是多线程安全问题

线程是随机调度,抢占式执行,这样的随机性会使程序的执行顺序产生变数,从而产生不同的结果,但是有时候,遇到不同的结果,认为不可接收,认为是bug
多线程代码引起的bug,这样的问题就是“线程安全问题”存在“线程安全问题的代码”,就称为“线程不安全”
线程不安全的例子:

java">public class Test19 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(()-> {for (int i = 0; i < 50000; i++) {count++;}});Thread thread2 = new Thread(()-> {for (int i = 0; i < 50000; i++) {count++;}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("count = " + count);}
}

thread1和thread2都对count这个变量进行了++操作,所以我们预期的结果是10w,但是由于出现了线程不安全的问题所以这里输出的结果是小于10w的
注意:每次运行后的结果都是不一样的,大部分情况都是大于5w,但是也有一部分情况是小于5w的
在这里插入图片描述
具体说明:
count++这个代码其实是3个cpu指令
1.把内存count中的数值,读取到cpu寄存器中,我们取个名字叫load
2.把寄存器中的值+1,还是继续保存在寄存器中,取个名字叫add
3.把寄存器上述计算的值2,写回到内存count里,取个名字叫save
两个线程并发的进行count++,多线程的执行,是随机调度,抢占式的执行模式

综上所述在实际并发执行的时候,两个线程执行指令的相对顺序就可能会出现多种情况,不同的执行顺序,得到的结果也就可能会存在差异
在这里插入图片描述
第一种和第二种最终的结果都是正确的,第三种虽然t1和t2都执行了count++操作,但是t1将t2线程中的count进行了覆盖,重新赋值,所以t2线程的操作就是无效的
出现结果小于5w的情况:
在这里插入图片描述
注意:多个线程并发执行的时候,具体指令执行的先后顺序,可能存在无数种情况

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

2.1 线程在系统中是随机调度,抢占式执行的

这个是线程不安全的罪魁祸首,万恶之源

2.2 多个线程同时修改同一个变量

一个线程修改同一个变量,没事
多个线程读取同一个变量,没事
多个线程修改不同变量,没事

2.3 线程针对变量的修改操作,不是“原子”的

“原子”指的是不可拆分的最小单位
count++这种不是原子操作,但是针对int/double进行赋值操作,在cpu中就是一个move指令

2.4 内存可见性问题

2.5 指令重排序

后面两个我们会在后面详细介绍

3 、如何解决线程安全问题

原因1:因为涉及到操作系统所以我们无法干预
原因2:这种做法在Java中不是很普适,只能针对一些特定的场景
原因3:是解决线程安全问题最普适的方法,我们可以通过加锁来解决线程安全问题

3.1 锁操作

关于锁操作的两个方面
1.加锁:t1加上锁之后,t2也尝试加锁,就会阻塞等待
2.解锁:直到t1解锁之后,t2才有可能拿到锁(加锁成功)
锁的主要特性:互斥
互斥指的是一个线程获取到锁之后,另一个线程也尝试加这个锁,就会阻塞等待(也叫锁竞争/锁冲突)
在代码中,我们可以创建多个锁,只有多个线程竞争同一把锁,才会产生互斥,针对不同的锁,则不会

3.2 synchronized关键字

synchronized关键字用于实现线程同步确保在同一时刻只有一个线程可以访问某个代码块或者方法
synchronized后面带上( ),括号里面写的就是“锁对象”
注意:锁对象的用途,有且只有一个,就是区分两个线程是否是针对同一个对象进行加锁,如果是就会出现锁竞争/锁冲突/互斥,就会引起阻塞等待,如果不是就不会出现锁竞争,也就不会阻塞等待
synchronized下面跟着{ },当进入到代码块就是给上述( )锁对象进行加锁操作,出代码块就是给上述( )锁对象进行解锁操作

java">public class Test19 {private static int count = 0;public static void main(String[] args) throws InterruptedException {//Java中随便拿一个对象,都可以做为加锁的对象Object locker1 = new Object();Thread thread1 = new Thread(()-> {for (int i = 0; i < 50000; i++) {synchronized (locker1) {count++;}}});Thread thread2 = new Thread(()-> {for (int i = 0; i < 50000; i++) {synchronized (locker1) {count++;}}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("count = " + count);}
}

运行结果:
在这里插入图片描述
此时我们通过加锁就得到了我们预期的结果

使用synchronized关键字修饰实例方法

java">class Counter {private int count = 0;synchronized public void add() {count++;}public int get() {return count;}
}
public class Test20 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread thread1 = new Thread(()-> {for (int i = 0; i < 50000; i++) {counter.add();}});Thread thread2 = new Thread(()-> {for (int i = 0; i < 50000; i++) {counter.add();}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("count = " + counter.get());}
}

使用synchronized关键字修饰静态方法

java">class Counter {private static  int count = 0;synchronized public static void add() {count++;}public int get() {return count;}
}
public class Test20 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread thread1 = new Thread(()-> {for (int i = 0; i < 50000; i++) {counter.add();}});Thread thread2 = new Thread(()-> {for (int i = 0; i < 50000; i++) {counter.add();}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("count = " + counter.get());}
}

4、不正确加锁引发的问题

4.1 一个加锁一个不加锁

java">public class Test19 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Thread thread1 = new Thread(()-> {for (int i = 0; i < 50000; i++) {synchronized (locker1) {count++;}}});Thread thread2 = new Thread(()-> {for (int i = 0; i < 50000; i++) {count++;}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("count = " + count);}
}

运行结果:
在这里插入图片描述
观察结果发现这种情况同样会发生多线程不安全的问题
原因:thread2没加锁,意味着即使thread1加锁了,thread2执行过程中没有任何阻塞,没有互斥,仍然会使thread1 ++到一半的时候,被thread2进来把结果覆盖掉
所有当我们需要对两个线程中且操作同一个方法或者代码块,同时加锁才能解决多线程安全的问题

4.2 可重入锁

同一个线程中,对一个对象进行多次加锁就叫做可重入锁

java">public class Test19 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Thread thread1 = new Thread(()-> {for (int i = 0; i < 50000; i++) {synchronized (locker1) {synchronized (locker1) {count++;}}}});Thread thread2 = new Thread(()-> {for (int i = 0; i < 50000; i++) {synchronized (locker1) {count++;}}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("count = " + count);}
}

假设thread1对代码块进行加锁,那么thread2就不能进行加锁操作,此时就会发生锁冲突,只有thread1解锁之后,threadd2才能进行加锁操作。对于thread1来说外层加完锁之后,此时内层在加锁之前就会判断当前是哪个线程对当前的代码块进行的加锁,如果是当前线程就会无视内层加锁这个操作继续往下执行,如果不是就会阻塞等待
所有当前运行结果是正确的:
在这里插入图片描述
注意:当加了多层锁的时候,代码要执行到最外层 } 花括号才会自动解锁,而不是内层的 } 括号解锁,所有内层的加锁是没用的,在最外层加锁就足够了

4.3 两个线程两把锁-死锁

死锁:两个或者多个线程(或进程)相互等待对方释放资源而造成的一种僵局,导致代码无法正常执行

java">public class Test21 {public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Object locker2 = new Object();Thread thread1 = new Thread(()-> {synchronized (locker1) {try {//引用sleep是为了更好的控制线程执行的顺序Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2) {System.out.println("thread1获取到两把锁");}}});Thread thread2 = new Thread(()-> {synchronized (locker2) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1) {System.out.println("thread2获取到两把锁");}}});thread1.start();thread2.start();thread1.join();thread2.join();}
}

在这里插入图片描述
在这里插入图片描述
运行结果:
在这里插入图片描述

4.4 死锁的四个特性

5.1 互斥特性

一个线程拿到锁之后,其他线程就得阻塞等待

5.2 不可抢占(不可被剥夺)

一个线程拿到锁之后,除非它自己主动释放锁,否则别人抢不走

5.3 请求和保持

一个线程拿到一把锁之后,不释放这个锁的前提下,再尝试获取其他锁

5.4 循环等待

多个线程获取多个锁的过程中,出现了循环等待,A等B,B又等A

4.5 如何避免死锁

1.锁具有互斥特性
2.锁不可抢占(不可被剥夺)
前面两种可以自己实现锁来打破,但是对于synchronized这样的锁是不行的
3.请求和保持,打破方法是不要让锁嵌套获取
4.循环等待,打破循环等待,即使出现嵌套也不会死锁,约定好加锁的顺序让所有的线程按照固定的顺序来获取锁


http://www.ppmy.cn/devtools/32439.html

相关文章

从零开始学AI绘画,万字Stable Diffusion终极教程(二)

【第2期】关键词 欢迎来到SD的终极教程&#xff0c;这是我们的第二节课 这套课程分为六节课&#xff0c;会系统性的介绍sd的全部功能&#xff0c;让你打下坚实牢靠的基础 1.SD入门 2.关键词 3.Lora模型 4.图生图 5.controlnet 6.知识补充 在第一节课里面&#xff0c;我们…

第19章 基于质量特性的测试技术

一、功能性测试 &#xff08;一&#xff09;测试方法 等价类边界值法因果图法判定表法场景法 &#xff08;二&#xff09;用例 1、正常用例 2、异常用例 &#xff08;三&#xff09;完备性 1、功能覆盖率 2、X1-A/B 功能覆盖率X&#xff1a;软件实际功能覆盖文档中所有…

Ubuntu Linux完全入门视频教程

Ubuntu Linux完全入门视频教程 UbuntuLinux完全入门视频教程1.rar UbuntuLinux亮全入门视频教程10.ra UbuntuLinux亮全入门视频教程11.ra UbuntuLinux完全入门视频教程12.ra UbuntuLinux亮全入门视频教程13.ra UbuntuLinux完全入门视频教程14.rar UbuntuLinux完全入门视频教程…

面试 Java 基础八股文十问十答第二十六期

面试 Java 基础八股文十问十答第二十六期 作者&#xff1a;程序员小白条&#xff0c;个人博客 相信看了本文后&#xff0c;对你的面试是有一定帮助的&#xff01;关注专栏后就能收到持续更新&#xff01; ⭐点赞⭐收藏⭐不迷路&#xff01;⭐ 1&#xff09;你觉得 Java 好在哪…

高扬程水泵的性能与应用领域 /恒峰智慧科技

在现代社会中&#xff0c;科技的发展为我们的生活带来了无数便利和可能性。其中&#xff0c;高扬程水泵作为一种高效能的水泵&#xff0c;其独特的设计使其在各个领域都有着广泛的应用&#xff0c;尤其是在森林消防中。 一、高扬程水泵的性能 1. 高扬程&#xff1a;高扬程水泵…

vue3+vite+js axios引用

先交代下基础版本&#xff1a; “node”&#xff1a;“V16.14.1” “vue”: “^3.4.21” “vite”: “^5.2.0” 安装&#xff1a;npm install axios --save在src目录下的utils文件夹创建一个request.js文件&#xff08;示例代码&#xff0c;仅供参考&#xff09;: //引入axio…

【综述】多核处理器芯片

文章目录 前言 Infineon处理器 AURIX™系列 TC399XX-256F300S 典型应用 开发工具 参考资料 前言 见《【综述】DSP处理器芯片》 Infineon处理器 AURIX™系列&#xff0c;基于TriCore内核&#xff0c;用于汽车和工业领域。 XMC™系列&#xff0c;基于ARM Cortex-M内核&…

NodeJs入门知识

**************************************************************************************************************************************************************************** 1、配置Node.js与npm下载&#xff08;精力所致&#xff0c;必有精品&#xff09; …