JavaEE 初阶篇-深入了解多线程安全问题(出现线程不安全的原因与解决线程不安全的方法)

🔥博客主页: 【小扳_-CSDN博客】
❤感谢大家点赞👍收藏⭐评论✍

文章目录

        1.0 多线程安全问题概述

        1.1 线程不安全的实际例子

        2.0 出现线程不安全的原因

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

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

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

        2.4 内存可见性

        2.5 指令重排序

        3.0 解决线程不安全问题(使用锁机制)

        3.1 synchronized 关键字可以作用的地方

        3.1.1 同步代码块

        3.1.2 同步实例方法

        3.1.3 同步静态方法

        3.2 join() 方法与 synchronized 关键字的区别

        4.0 加锁不合理所引发的问题

        4.1 对于一个加锁与一个没有加锁的两个线程随机调度执行同一个代码块或者方法的情况

        4.2 嵌套相同的锁 - 可重入锁

        4.3 两个线程两把锁 - 死锁

        4.4 死锁的四个必要条件

        4.4.1 互斥条件

        4.4.2 不可剥夺条件

        4.4.3 请求保持条件

        4.4.4 循环等待条件

        4.5 如何避免死锁?


        1.0 多线程安全问题概述

        多线程安全问题是指在多线程环境下,多个线程同时访问共享资源可能导致的数据不一致、数据竞争、死锁等问题。

        1.1 线程不安全的实际例子

        在多线程中,很容易就会出现多线程问题,从而引发多线程安全问题。

先看以下代码:

    public static long count = 0;public static void main(String[] args) throws InterruptedException {Object o = new Object();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}

        一般来说,t1 和 t2 都对 count 这个变量进行 count++ 这个操作,那么 count 最后的输出结果按理来说应该为 10万。但是输出的结果是不一定为 10 万,而且等于 10 万的概率非常非常非常小。

运行结果:

        每次的运行结果都是不一样的,很大概率都是在 5 万到 10 万之间“徘徊”。也有可能会小于 5 万。

        出现了以上的结果,都是因为多线程抢占式执行随机调度,从而导致的结果。

具体分析:

        在 CPU 中执行 count++ 这一操作,大致需要执行三条指令:

        1)load:将内存中的数据读取加载到 CPU 的寄存器中。

        2)add:在寄存器中的值 +1 操作。

        3)save:把寄存器中的值写回到内存中。

        现在 t1 与 t2 两个线程并发的进行 count++ ,多线程的执行是随机调度,抢占式的执行模式。有可能会出现以下几种情况:

        出现可能 1 或者可能 2 这两次 count++ 的最终结果都是 2 ,因此是正确的。而对于可能 3 这种情况,虽然说,t1 与 t2 都完成了 count++ 的操作,但是,对于可能 3 这种情况,在 t2 完成 count++ 之后,count 由 0 改为 1 ,再把值写回到内存之后,但是 t1 来说,同样也是把 count 由 0 改为 1 ,写回到内存中。简单来说,t1 将 t2 线程中 count 进行了一次覆盖,重新赋值,所以 t2 这个线程的操作是无效操作。

        出现的情况有无数种,不可预计的。之所以说,最后出现的结果为 10 万的概率是非常非常小的,几乎没有可能吧。

对于出现小于 5 万的情况:

          按理来说,进行了三次 count++ 操作,最后的结果应该为: count == 3,但是这里最后的结果:count == 1。这就是有可能出现 count 小于 5万的可能,出现数越加越小了。

        以上就是属于多线程引发的线程安全问题。

        2.0 出现线程不安全的原因

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

        这是导致线程不安全的“罪魁祸首,万恶之源”,不能去改变这个机制。

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

        当多个线程同时修改同一个变量时,可能会导致数据竞争和结果不确定性。这种情况下,需要采取线程安全的措施来确保数据的一致性。

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

        count++ 这种,不是原子操作,在 cpu 执行 count++ 操作需要三条指令。

        2.4 内存可见性

        2.5 指令重排序

        3.0 解决线程不安全问题(使用锁机制)

        锁机制可以确保在任意时刻只有一个线程可以访问共享资源,从而避免数据竞争和保证数据的一致性。

        可以使用 synchronized 关键字等来实现锁机制。

代码如下:

    public static long count = 0;public static void main(String[] args) throws InterruptedException {Object o = new Object();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o){count++;}}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}

        通过 synchronized 关键字,把 count++ 这个操作进行了加锁,对于 o 对象可以理解为一个标志,一个锁的标识,当进入 {} 那一刻,就会加上锁,执行完代码块中的代码后,退出 {} 时,会自动解锁。 

        现在对于 t1 与 t2 线程来说,在每一次 for 循环之后,会抢占 “加锁” 随机调度,两个线程抢占的机会是一样的,比如说 t1 抢占到了“加锁”,那么 t2 想要再次对 count++ 加锁时,先会判断,判断当前加锁的线程是哪一个线程,如果不是自己线程,那么就会阻塞等待 t1 线程。等待 t1 执行完毕之后,t1 与 t2 会继续抢占对 count++ 这个操作进行“加锁”处理,一直循环往复。

        这样就保证了 CPU 在执行 3 条指令的时候,不会被其他线程“打扰到”。每一次都是如

此:

最后的运行结果:

        此时多线程问题是安全的。

补充:

        1)锁本质上也是操作系统提供的功能,内核提供的功能,通过 API 给应用程序 JVM 对于这样的系统 API 又进行封装。

        2)锁对象的用途,有且只有一个,就是用来区分,判断两个线程是否是针对同一个对象加锁,如果是,就会出现锁竞争/锁冲突/互斥,就会引起阻塞等待;如果不是,就不会出现锁竞争,也就不会阻塞等待。

        和对象的具体是什么类型,和它里面的属性、方法,对于接下来操作这个对象统统没有任何关系。所以可以将类似 o 对象简单理解为一个标识,一个工具。

        3)锁涉及的核心有两个:加锁、解锁

        主要的特性:互斥,一个线程获取到锁之后,另一个线程也尝试加这个锁,就会阻塞等待(锁竞争/锁冲突)。

        在代码中,可以创建出多个锁,只有多个线程竞争同一把锁,才会产生互斥,针对不同的锁,则不会。

        3.1 synchronized 关键字可以作用的地方

        3.1.1 同步代码块

        使用 synchronized 关键字修饰代码块,可以指定对象作为锁,确保在同一时刻只有一个线程可以访问该代码块。其他线程需要等待获取锁后才能执行代码块。

    public static long count = 0;public static void main(String[] args) throws InterruptedException {Object o = new Object();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {//作用于代码块中synchronized (o){count++;}}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {//作用于代码块中synchronized (o){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}

        3.1.2 同步实例方法

        使用 synchronized 关键字修饰实例方法,可以确保在同一时刻只有一个线程可以访问该实例方法。其他线程需要等待当前线程执行完毕后才能访问。

代码如下:

public class demo11 {public synchronized void add(){count++;};public long get(){return count;};public static long count = 0;public static void main(String[] args) throws InterruptedException {demo11 demo = new demo11();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {demo.add();}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {demo.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(demo.get());}
}

        3.1.3 同步静态方法

        使用 synchronized 关键字修饰静态方法,可以确保在同一时刻只有一个线程可以访问该静态方法。其他线程需要等待当前线程执行完毕后才能访问。

代码如下:

public class demo11 {public synchronized static void add(){count++;};public static long get(){return count;};public static long count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {demo11.add();}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {demo11.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(demo11.get());}
}

        3.2 join() 方法与 synchronized 关键字的区别

        1)join() 是 Thread 类的方法,用于等待调用该方法的线程执行完成。当一个线程调用另一个线程的 join() 方法时,它会被阻塞,直到被调用的线程执行完成。

        2)synchronized 关键字用于实现线程同步确保在同一时刻只有一个线程可以访问某个代码块或方法。

         总的来说,join 用于线程之间的协作和等待,而 synchronized 用于实现线程之间的同步和互斥访问共享资源。

        4.0 加锁不合理所引发的问题

        4.1 对于一个加锁与一个没有加锁的两个线程随机调度执行同一个代码块或者方法的情况

        对于这种情况来说,同样会导致多线程安全问题。因为对于一个加锁与另一个没有加锁的情况,这两个线程之间没有锁竞争或者产生互斥,所以还是会出现多线程安全问题。

代码如下:

public class demo9 {public static long count = 0;public static void main(String[] args) throws InterruptedException {Object o = new Object();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o){count++;}}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

运行结果:

        以上代码中即使有一个线程加上了锁,同样也是跟没有加锁的代码本质是一样的。因此,需要对两个线程中且操作同一个代码块或者方法进行同时加锁处理,才会解决多线程的安全问题。

        4.2 嵌套相同的锁 - 可重入锁

        在同一个线程中,嵌套同一个锁被称为可重入锁

代码如下:

        在外层加完锁之后,在内层继续加了相同的锁。再来了解加锁的详细过程:两个线程随机调度执行,假设 t1 对该代码块加锁,而 t2 就不能加锁了,此时产生了锁竞争,t2 需要阻塞等待 t1 执行后解锁后,才能继续去“抢夺”上锁;对于 t1 来说外层加完锁之后,此时内层加锁之前需要判断当前是那个线程对当前的代码块上锁,如果是当前线程加锁了,那么内层加锁这个操作就是为无,可以继续往下执行,注意这里没有产生锁竞争;如果不是当前线程加锁了,就会阻塞等待。

所以可重入锁是安全的,运行结果: 

        补充:解锁是执行到外层 } 花括号结束之后,才会自动解锁,而不是执行到内层的 } 花括号解锁。所以,内层加锁其实是没有用的,正常来说,有最外面加锁就足够了,之所以要搞上述操作,就是担心不小心把代码写错从而搞出“死锁”,目的就是避免程序员粗心大意。

        4.3 两个线程两把锁 - 死锁

        死锁是系统中的多个线程或进程相互等待对方释放资源,从而陷入僵局无法继续执行的状态。

代码如下:

public class demo12 {public static void main(String[] args) {Object o1 = new Object();Object o2 = new Object();Thread t1 = new Thread(()->{synchronized (o1){//这里用到 sleep 方法的原因是因为,//保证 t2 线程执行完:对 o2 进行加锁操作try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (o2){System.out.println("正在执行 t1 线程");}}});Thread t2 = new Thread(()->{synchronized (o2){//这里用到 sleep 方法的原因是因为,//保证 t1 线程执行完:对 o1 进行加锁操作try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (o1){System.out.println("正在执行 t2 线程");}}});t1.start();t2.start();}
}

        此时 t1 线程对 o1 加锁了,t2 线程对 o2 加锁了,对于 t1 来说,想要继续往下执行,需要 t2 对 o2 解锁,想要 t2 对 o2 解锁对话,需要 t1 对 o1 解锁。想要 t1 解锁的话,还是需要 t2 对 o2 解锁。。。此时成了很尴尬的情况,对方要需要对方的资源,双方都相互等待对方释放资源,从而僵持住了,无法执行下去。这样就造成了一个死锁。

通过 Jconsole 可执行程序观察:

已经检测到了死锁

运行结果:

        程序一直在运行中

        4.4 死锁的四个必要条件

        4.4.1 互斥条件

        资源只能被一个线程或进程所持有,其他线程无法同时访问。

        4.4.2 不可剥夺条件

        线程已经获取的资源在未使用完之前不能被其他线程所抢占。

        4.4.3 请求保持条件

        线程可以持有一些资源并继续请求其他资源。

        4.4.4 循环等待条件

        每个线程都在等待其他线程所持有的资源,形成一个循环等待的情况。

        4.5 如何避免死锁?

        只需要破环任意一个满足死锁的必要条件即可。

        1)对于互斥条件来说,不能破坏,所以不用考虑这种情况。

        2)对于不可剥夺条件,破坏该条件的方法是:如果一个线程无法获取资源,可以释放已经持有的资源,避免长时间占用资源。

        3)对于请求保持条件,破坏该条件的方法是:一次性获取所有需要的资源。

        4)对于循环等待条件,破坏该条件的方法是:按照固定顺序来获取资源,避免形成循环等待。

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

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

相关文章

游戏行业行业竞争越来越激烈,遇到DDoS攻击遭受严重损失该如何解决

近年来&#xff0c;我们见证了数字化的快速发展&#xff0c;随着这样的发展&#xff0c;网络的威胁也逐渐增多&#xff0c;在网络攻击门槛不断降低&#xff0c;行业竞争越来越激烈&#xff0c;游戏行业的DDoS攻击如雨点般密集&#xff0c;在整个DDoS攻击的份额中&#xff0c;游…

SpringAMQP-Exchange交换机

1、Fanout-Exchange的特点是&#xff1a;和它绑定的消费者都会收到信息 交换机的作用是什么? 接收publisher发送的消息将消息按照规则路由到与之绑定的队列不能缓存消息&#xff0c;路由失败&#xff0c;消息丢失FanoutExchange的会将消息路由到每个绑定的队列 声明队列、交…

【实验报告】--基础VLAN

【VLAN实验报告】 一、项目背景 &#xff08;为 Jan16 公司创建部门 VLAN&#xff09; Jan16 公司现有财务部、技术部和业务部&#xff0c;出于数据安全的考虑&#xff0c;各部门的计算机需进 行隔离&#xff0c;仅允许部门内部相互通信。公司拓扑如图 1 所示&#xff0c; …

http和https的工作原理是什么?

HTTP&#xff08;HyperText Transfer Protocol&#xff09;和HTTPS&#xff08;HyperText Transfer Protocol Secure&#xff09;是两种用于在互联网上传输数据的主要协议&#xff0c;它们均用于在客户端&#xff08;通常是Web浏览器&#xff09;与服务器之间交换信息。尽管它们…

.NET CORE 分布式事务(二) DTM实现TCC

目录 引言&#xff1a; 1. TCC事务模式 2. TCC组成 3. TCC执行流程 3.1 TCC正常执行流程 3.2 TCC失败回滚 4. Confirm/Cancel操作异常 5. TCC 设计原则 5.1 TCC如何做到更好的一致性 5.2 为什么只适合短事务 6. 嵌套的TCC 7. .NET CORE结合DTM实现TCC分布式事务 …

访学博后须知|携带手机等电子产品入境美国注意事项

美国对携带手机等电子产品入境有着严格的规定&#xff0c;因此知识人网小编提醒拟出国做访问学者、博士后或联合培养的博士生了解以下注意事项&#xff0c;尽量减少不必要的麻烦。 随着互联网的普及&#xff0c;手机等电子产品在人民生活中占有不可或缺的地位。因为研究和工作需…

海量数据处理项目-账号微服务和流量包数据库表+索引规范(下)

海量数据处理项目-账号微服务和流量包数据库表索引规范&#xff08;下&#xff09; 第2集 账号微服务和流量包数据库表索引规范讲解《下》 简介&#xff1a;账号微服务和流量包数据库表索引规范讲解 账号和流量包的关系&#xff1a;一对多traffic流量包表思考点 海量数据下每…

ES6 学习(二)-- 字符串/数组/对象/函数扩展

文章目录 1. 模板字符串1.1 ${} 使用1.2 字符串扩展(1) ! includes() / startsWith() / endsWith()(2) repeat() 2. 数值扩展2.1 二进制 八进制写法2.2 ! Number.isFinite() / Number.isNaN()2.3 inInteger()2.4 ! 极小常量值Number.EPSILON2.5 Math.trunc()2.6 Math.sign() 3.…

仓库规划(plan)

明天就要考试了&#xff0c;但是我正处于一点都不想学的状态 高考前我也是这样的 逆天 代码如下&#xff1a; #include<vector> #include<cstdio> using namespace std; int n, m; struct Node{int id;vector<int> d;bool operator<(const Node &t…

平台介绍-搭建赛事运营平台(8)

平台介绍-搭建赛事运营平台&#xff08;5&#xff09;提到了字典是分级的&#xff0c;本篇具体介绍实现。 平台级别的代码是存储在核心库中&#xff0c;品牌级别的代码是存储在品牌库中&#xff08;注意代码类是一样的&#xff09;。这部分底层功能封装为jar包&#xff0c;然后…

matlab BP神经网络回归预测(套用任何数据不改代码,最后一列是标签)

部分代码&#xff1a; %% BP神经网络 % 清空环境变量 关闭打开过的图表 clear all;clc;close all %% 导入数据 dataxlsread(data1.xlsx); %% 设置训练集数量 num_rowsize(data,1) %数据集行数 n_trainsfloor(num_row*0.8) %按比例求训练集数目 % n_trains150 …

JDK和IntelliJ IDEA下载和安装及环境配置教程

一、JDK下载&#xff08;点击下方官网链接&#xff09; Java Downloads | Oracle 选择对应自己电脑系统往下拉找到自己想要下载的JDK版本进行下载&#xff0c;我下的是jdk 11&#xff0c;JDK有安装版和解压版&#xff0c;我就直接下安装版的了。 .exe和.zip的区别&#xff1a…

通过MobaXterm工具可视化服务器桌面

一、MobaXterm工具 MobaXterm是一款功能强大的远程连接工具&#xff0c;可以连接到各种类型的服务器&#xff0c;包括Linux、Windows和MacOS。支持多种协议&#xff0c;包括SSH、RDP、VNC和Telnet MobaXterm可以通过X11转发功能可视化服务器桌面。 二、MobaXterm工具可视化服务…

011——人体感应模块驱动开发(SR501)

目录 一、 模块简介 二、 工作原理 三、 软件及验证 一、 模块简介 人体都有恒定的体温&#xff0c;一般在 37 度&#xff0c;所以会发出特定波长 10uM 左右的红外线&#xff0c;被动式红外探头就是靠探测人体发射的 10uM 左右的红外线而进行工作的。 人体发射的 10…

架构师之路--Docker的技术学习路径

Docker 的技术学习路径 一、引言 Docker 是一个开源的应用容器引擎&#xff0c;它可以让开发者将应用程序及其依赖包打包成一个可移植的容器&#xff0c;然后在任何支持 Docker 的操作系统上运行。Docker 具有轻量级、快速部署、可移植性强等优点&#xff0c;因此在现代软件开…

Hides for Mac:应用程序隐藏工具

Hides for Mac是一款功能强大的应用程序隐藏工具&#xff0c;专为Mac用户设计。它能够帮助用户快速隐藏当前正在运行的应用程序窗口&#xff0c;保护用户的隐私和工作内容&#xff0c;避免不必要的干扰。 软件下载&#xff1a;Hides for Mac下载 Hides for Mac的使用非常简单直…

电脑换屏总结——关于我把电脑砸了这件事!

大家好&#xff0c;我是工程师看海&#xff0c;很高兴和各位一起分享我的原创文章&#xff0c;喜欢和支持我的工程师&#xff0c;一定记得给我点赞、收藏、分享哟。 加微信[chunhou0820]与作者进群沟通交流。 【淘宝】https://m.tb.cn/h.5PAjLi7?tkvmMLW43KO7q CZ3457 「运放秘…

vite+vue3使用模块化批量发布Mockjs接口

在Vue3项目中使用Mock.js可以模拟后端接口数据&#xff0c;方便前端开发和调试。下面是使用vitevue3使用模块化批量发布Mockjs接口的步骤&#xff1a; 1. 安装Mock.js 在Vue3项目的根目录下&#xff0c;使用以下命令安装Mock.js&#xff1a; npm install mockjs --save-dev …

项目亮点—动态线程池管理工具

问题 你是否在项目中使用线程池遇到过以下问题&#xff1f; 1.创建线程池核心参数不好评估&#xff0c;随着业务流量的波动&#xff0c;极有可能出现生产故障。 2.不支持优雅关闭&#xff0c;当项目关闭时&#xff0c;大量正在运行的线程池任务被丢弃。 3.不支持运行时监控…

Linux安装wine

#教程 一直以来&#xff0c;我运行双系统&#xff0c;有两个软件必须在window下运行&#xff0c;一个是wind金融终端&#xff0c;一个是通达信金融终端&#xff0c;现已解决这两个软件在linux&#xff08;debian系&#xff09;环境下运行问题&#xff0c;记录如下&#xff1a;…