JavaEE 第4节 线程安全问题

小贴士:

本节题目所述的主题其实非常的庞大,如果要细讲起来,一篇博客远远不够,本篇博客只会每个方面的内容做一个简要描述详细的内容在后续同专栏博客中都会涉及到的,如果有需要可以一步到本专栏的其他博客

正文开始:

一、什么线程安全问题?

示例演示:

这里用一个直观的代码来展示一个经典的线程安全问题:

public class demo1 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {count++;}});Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {count++;}});//分别通过两个线程对count++t1.start();t2.start();t1.join();//join方法的作用是主线程运行到这了,先等待t1线程结束,在回来运行主线程。t2.join();//t2线程同理System.out.println("count="+count);//输出最后的count值}
}

按照正常的逻辑,count值因该是10000,可是:

很奇怪😕

这实际上就是出现了线程安全问题。

是什么原因导致这些问题的呢?

我们接下来将会详细讲解。

二、造成线程安全问题的主要原因

主要原因:

1、操作系统对线程的调度,在程序运行角度看是“随机的”(抢占式执行)

2、代码结构,即多个线程同时修改同一个变量。

3、修改变量这个操作不是原子性*的。

4、指令重排列(Instruction Reordering,后序章节详细讲解)

5、内存可见性(Memory Visibility,后续章节详细讲解

6、线程饿死(Thread Starvation,后续章节详细讲解

7、死锁(Deadlock,后序章节详细讲解)


注:
原子性*的意思是对于一个操作,结果只能是做了和没做两种状态,不能出现第三种状态。

刚才示例出现线程安全问题的原因就是1、2、3点导致的:

在代码中我们知道t1和t2两个线程时并发执行的,并且都对count变量进行++操作

而在CPU的视角看,count++操作要分成三步(不是原子的):

1)load:把count对应的内存数据写入寄存器。

2)add:逻辑运算单元对数据进行++操作。

3)save:把新的值重新写入count变量的内存。

t1和t2两个线程并发执行,都在不断按照上面这三步指令执行,在系统“随机”调度的过程中就很可能出现这样一种情况:

某一时刻,t1和t2同时load了count的内存数据,并且两个线程load的count值时一样的,然后他们分别对count++,最后写入内存(save)。会过头来我们发现,在这两个线程都运行完一次后,count只进行了一次++操作!


在深入问大家一个问题,程序中的count有没有可能小于5000呢?

答案是可能的

可能的情况举例:

t1和t2都只能对count++5000次,倘若又这样一个情况,t1刚开始被调度,读取到的count值是0,然后由于抢占式执行,t2开始被调度并且被多次连续调度,导致最后t2线程执行了4999次,之后t1又开始被调度把count=0写回原来的内存(形成了覆盖),然后t2又被调度了把count=0读取到逻辑运算单元,这是又由于抢占式执行,t2停止运作,t1开始被连续调度执行了5000,count被修改成了5000

现在只剩下t2还没有执行了,t2把count=0(在t2的逻辑运算单元上)++,对count进行覆写,count竟然还变成了1!

三、线程安全问题的解决办法

1、给线程加锁

像刚才的示例,我们可以通过设置多个变量的方式进行解决:

public class demo1 {public static int count = 0;public static int count1=0;public static int count2=0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {count1++;}});Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {count2++;}});//分别通过两个线程对count++t1.start();t2.start();t1.join();t2.join();count=count1+count2;System.out.println("count="+count);}
}

不过这不是JAVA解决线程安全问题的主流方式,了解即可。

在JAVA中,主流解决线程安全问题的方式是对线程进行“加锁”的操作。

什么是锁?

刚才讲解线程安全问题的原因时,我们提到了原子性、修改同一个变量这两个关键字,这里所说的锁实际上就是把一些非原子性的程序“锁”起来,让它变成原子性的,这样线程安全问题就被解决了。

比如刚才的t1和t2线程,都对count进行++操作。但是由于系统的“随机”调度,两个线程的load、add、save操作是相互穿插进行的,数据的修改很可能会出错。
而现在把[load、add、save]这个非原子性的++操作进行“上锁”,保证要么++操作成功,要么什么都没有操作,既++操作变成了原子性的。
通过锁的这种操作,两个线程的【load、add、save】🔒就不可能穿插执行了,因为必须完成【】🔒内的操作,才能去执行另一个线程的任务。
这样线程安全问题就得到了解决。

synchronized关键字(加锁的工具)

synchronized基本用法:

Java提供了 synchronized 关键字 (监视器锁-monitor lock)来完成加锁操作。

接下来通过synchronized关键字,解决上面的线程安全问题:

public class demo1 {public static int count = 0;public static Object object1 = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (object1) {//括号内填写一个实例对象,任何类性的对象都是可以的!!count++;}}});Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {//括号内填写一个实例对象,任何类性的对象都是可以的!!synchronized (object1) {//代码块中填写,需要原子化的程序。count++;}}});//分别通过两个线程对count++t1.start();t2.start();t1.join();t2.join();System.out.println("count=" + count);}
}

此时运行结果count=10000

synchronized除了上述写法,还可以通过修饰静态方法或者成员方法的方式,实现加锁:

public class Threads {static int count = 0;//实现加锁private synchronized static void add() {count++;}public static void main(String[] args) throws InterruptedException {Thread thread1=new Thread(()->{for (int i = 0; i <5000 ; i++) {add();}});Thread thread2=new Thread(()->{for (int i = 0; i <5000 ; i++) {add();}});thread1.start();thread2.start();Thread.sleep(1000);//这样可以极大增大 先让thread1和thread2两个线程先执行完,在打印count的概率System.out.println(count);}
}

成员方法是同理的,这里就不做过多演示了。

synchronized的一些基本特性:
   1)互斥(Mutual Exclusion)

进入synchronized代码块内,相当于上锁
退出synchronized代码块,相当于 解锁
对于同一个对象,如果一个线程上了锁,那么其他线程必须等待这个线程解锁,才能运行:

锁外其他的线程就处在BLOCK的等待状态。


图中的同一个对象是什么意思?

在刚才的代码演示中,t1和t2两个线程的synchronized括号里,填写的都是同一个对象。

如果两个线程填写不同的对象,跟没加锁没有区别,最后的count大概率也不可能等于一万。

也就是说,对于同一个对象加锁,锁对于一个线程来说才是有效的,或者说是存在的。

比如,我们对上面的代码进行简单的修改,t1线程和t2线程两个锁对象不同:

public class demo1 {public static int count = 0;public static Object object1 = new Object();public static Object object2 = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (object1) {count++;}}});Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (object2) {count++;}}});//分别通过两个线程对count++t1.start();t2.start();t1.join();t2.join();System.out.println("count=" + count);}
}

其中一个运行结果:

2)可重入(Reentrant)

如果重复对同一个线程进行这种加锁会怎么样:

  Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (object1) {//第一次锁了synchronized(object1){//第二次我在锁?count++;}}}});

我们来慢慢分析:

第一次加锁:

本来t1线程可以安全的上侧所的,但是他还不放心,于是synchronized代码块里,又上了一次锁:

所以根据上面的逻辑,重复对一个线程针对同一个对象加锁是会出现锁被“焊死”的情况的(也就是死锁

这种重复锁导致的死锁,只会出现在C++\Python等其他编程语言中,Java不会,因为synchronized关键字会对这个情况进行判断,不会对相同对象的相同线程进行重复上锁

具体代码举例:

//先清楚标志位,然后抛出异常
public class Threads {static int count = 0;//实现加锁private synchronized static void add() {count++;}public static void main(String[] args) throws InterruptedException {Thread thread=new Thread(()->{synchronized (Threads.class){synchronized (Threads.class){System.out.println("在第二个锁的内部");}}});Thread.sleep(1000);thread.start();thread.join();System.out.println("thread线程结束");}}

以上代码在逻辑上是错误的,因为对同一个对象同一个线程重复上锁了,但是程序并没有卡主:

原因就是synchronized关键字会自动识别是重复上锁,如果有只会上锁一次。

那么如果没有synchronized的这个特性,程序会怎么样呢?

如图:

程序将会永远的停留在第22行和23行之间,在第22行第一次上锁后,程序需要等待第一次锁的解锁,才能在23行位置进行在次上锁,这样就形成了一个逻辑闭环,循环依赖,永远无法退出!(C++\Python等这些语言就有可能出现这种状况)


额外知识补充:

synchronized关键字是JVM提供的功能,synchronized底层实现就是依靠JVM中C++代码调用操作系统的API来实现的。而这些操作系统的API又是通过CPU上特殊的指令来实现上锁、解锁的。

2、volatile关键字

这个关键字是专门解决内存可见性问题的,这里不做过多解释,同专栏后续博客有详细讲解。

3、wait和notify方法

这两个方法是Object类自带的,用于解决线程饿死问题,这里不做过多解释,同专栏后续博客有详细讲解。


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

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

相关文章

python运行js之execjs基本使用

python运行js之execjs基本使用 现在大部分网站都使用JS加密和JS加载的情况&#xff0c;数据并不能直接被抓取出来&#xff0c;这时候就需要使用第三方类库来执行JS语句。 官网&#xff1a;https://pypi.org/project/PyExecJS/ 使用前提&#xff1a;电脑需要安装 Node.js 一、安…

最新口型同步技术EchoMimic部署

EchoMimic是由蚂蚁集团推出的一个 AI 驱动的口型同步技术项目&#xff0c;能够通过人像面部特征和音频来帮助人物“对口型”&#xff0c;生成逼真的动态肖像视频。 EchoMimic的技术亮点在于其创新的动画生成方法&#xff0c;它不仅能够通过音频和面部关键点单独驱动图像动画&a…

【星闪开发连载】WS63E 星闪开发板和hi3861开发板的对比

此次星闪开发者体验官活动使用的开发板都是NearLink_DK_WS63E开发板&#xff0c;它和NearLink_DK_WS63开发板的区别在于具有雷达感知功能。从开发板的照片也可以看到WS63E有一个雷达天线接口。 我们把WS63E开发板和hi3861开发板的功能做了简单的对比&#xff0c;见下表。 参数…

用户看广告获取密码访问网页内容流量主模式源码

简介&#xff1a; 全开源付费进群流量主模式&#xff0c;用户看广告获取密码访问网页内容&#xff0c;网站生成内容&#xff0c;用户需要浏览内容跳转至小程序&#xff0c;观看广告后获取密码&#xff0c;输入密码查看网页内容。 与之前得9.9付费进群区别就是内容体现在了网页…

iPhone苹果手机Safari浏览器怎么收藏网页?

iPhone苹果手机Safari浏览器怎么收藏网页? 1、iPhone苹果手机上找到并打开Safari浏览器&#xff0c;并访问要收藏的网页&#xff1b; 2、打开网页后&#xff0c;点击导航上的更多功能&#xff1b; 3、在更多里&#xff0c;找到并点击添加到个人收藏&#xff0c;完成储存即可添…

JavaSE面试篇章——一文干破Java集合

文章目录 Java集合——一文干破集合一、集合的理解和好处1.1 数组1.2 集合 二、集合的框架体系三、Collection接口和常用方法3.1 Collection接口实现类的特点3.2 Collection接口遍历元素方式1-使用Iterator(迭代器)3.2.1 基本介绍3.2.2 迭代器的执行原理3.2.3 Iterator接口的方…

java基础 之 equals和==的区别

文章目录 浅谈“”特点比较基本类型比较引用类型 浅谈“equals”背景和使用重写equals自定义类为什么需要重写equals方法 总结附录代码及文章推荐 前言&#xff1a; 1、8大基本数据类型&#xff0c;它们的值直接代表了某种数据&#xff0c;不是对象的实例&#xff0c;不能使用n…

关于企微群聊天工具功能的开发---PHP+JS+CSS+layui (手把手教学)

文章目录 前言准备工作PHP代码示例前端代码示例 主要是js踩的小坑&笔记最终达成的效果总结 前言 公司要求开发企微群聊天工具。首先一个客户一个群&#xff0c;其余群成员都是公司销售、设计师、工长、售后等人员。要求开发一个群聊天工具&#xff0c;工长点击进来以后就可…

ReentrantLock源码分析

文章目录 一、AQS1、state属性2、等待队列3、条件变量 二、ReentrantLock1、非公平锁实现原理1.1 获取锁1.2 释放锁1.3 可重入原理1.4 可打断原理不可打断可打断 1.5 公平锁实现原理1.6 条件变量原理awaitsignal 一、AQS AQS全称是 AbstractQueuedSynchronizer&#xff0c;是阻…

Python面试宝典第27题:全排列

题目 给定一个不含重复数字的数组nums&#xff0c;返回其所有可能的全排列 。备注&#xff1a;可以按任意顺序返回答案。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3] 输出&#xff1a;[[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]] 示例 2&#xff1a; 输…

FPGA开发——数码管的使用(二)

一、概述 在上一篇文章中我们针对单个数码管的静态显示和动态显示进行了一个设计和实现&#xff0c;这篇文章中我们针对多个数码管同时显示进行一个设计。这里和上一篇文章唯一不同的是就是数码管位选进行了一个改变&#xff0c;原来是单个数码管的显示&#xff0c;所以位选就直…

详细说明Java中Map和Set接口的使用方法

Map与Set的基本概念与场景 Map和set是一种专门用来进行搜索的容器或者数据结构&#xff0c;其搜索的效率与其具体的实例化子类有关。以前常见的搜索方式有&#xff1a; 1. 直接遍历&#xff0c;时间复杂度为O(N)&#xff0c;元素如果比较多效率会非常慢。 2. 二分查找&#x…

WordPress网站被入侵,劫持收录事件分析

7.15&#xff0c;网站被入侵&#xff0c;但是直到7月17日&#xff0c;我才发现被入侵。 16日&#xff0c;17日正常更新文章&#xff0c;17日查询网站收录数据时&#xff0c;在站长资源平台【流量与关键词】查询上&#xff0c;我发现了比较奇怪的关键词。 乱码关键词排名 起初…

JavaDS —— AVL树

前言 本文章将介绍 AVL 树的概念&#xff0c;重点介绍AVL 树的插入代码是如何实现的&#xff0c;如果大家对 AVL 树的删除&#xff08;还是和二叉搜索树一样使用的是替换删除法&#xff0c;然后需要判断是否进行旋转调整&#xff09;感兴趣的话&#xff0c;可以自行去翻阅其他…

关于Unity转微信小程序的流程记录

1.准备工作 1.unity微信小程序转换工具&#xff0c;minigame插件&#xff0c;导入后工具栏出现“微信小游戏" 2.微信开发者工具稳定版 3.MP微信公众平台申请微信小游戏&#xff0c;获得游戏appid 4.unity转webgl开发平台&#xff0c;Player Setting->Other Setting…

市场主流 AI 视频生成技术的迭代路径

AI视频生成技术的迭代路径经历了从GANVAE、Transformer、Diffusion Model到Sora采用的DiT架构&#xff08;TransformerDiffusion&#xff09;等多个阶段&#xff0c;每个阶段的技术升级都在视频处理质量上带来了飞跃性的提升。这些技术进步不仅推动了AI视频生成领域的快速发展&…

评估生成分子/对接分子的物理合理性工具 PoseBusters 评测

最近在一些分子生成或者对接模型中&#xff0c;出现了新的评估方法 PoseBusters&#xff0c;用于评估生成的分子或者对接的分子是否符合化学有效性和物理合理性。以往的分子生成&#xff0c;经常以生成分子的有效性、新颖性、化学空间分布&#xff0c;与口袋的结合力等方面进行…

微软蓝屏事件揭示的网络安全深层问题与未来应对策略

目录 微软蓝屏事件揭示的网络安全深层问题与未来应对策略 一、事件背景 二、事件影响 2.1、跨行业连锁反应 2.2、经济损失和社会混乱 三、揭示的网络安全问题 3.2、软件更新管理与风险评估 3.2、系统复杂性与依赖关系 3.3、网络安全意识与培训 四、未来的网络安全方向…

网络云相册实现--nodejs后端+vue3前端

目录 主页面 功能简介 系统简介 api 数据库表结构 代码目录 运行命令 主要代码 server apis.js encry.js mysql.js upload.js client3 index.js 完整代码 主页面 功能简介 多用户系统&#xff0c;用户可以在系统中注册、登录及管理自己的账号、相册及照片。 每…

众人帮蚂蚁帮任务平台修复版源码,含搭建教程。

全修复运营版本的任务平台&#xff0c;支持垂直领域细分&#xff0c;定向导流&#xff0c;带有排行榜功能&#xff0c;任务发布上传审核&#xff0c;用户信用等级&#xff0c;充值接口等等均完美可用。支付对接Z支付免签接口&#xff0c;环境配置及安装教程都已经打包。 搭建环…