【JavaEE】多线程(3)

首先回顾一下线程不安全的原因:

  1. 线程是随机调度,抢占式执行的
  2. 修改共享数据,多个线程修改同一个变量
  3. 多个线程修改共享数据的操作不是原子性,(count++是3个CPU指令,但是赋值操作就是原子性的)
  4. 内存可见性问题
  5. 指令重排序

前三点已做讲解,接下来对最后两点进行讲解

一、内存可见性问题

1.1 引入概念

先来看下面的代码:

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

上述代码就是t1线程来读count,t2来修改count,以原来的逻辑来看:当把count修改为一个非0的值后,t1线程就会结束

输入之后发现,程序没有任何反应,说明t1线程并没有结束,接下来我们仍然站在指令的角度来解释,t1线程中的循环条件count == 0相当于两个指令

  • load:读取内存中的数据到CPU寄存器
  • cmp:比较寄存器中的数据,条件成立就继续执行循环体中的逻辑,不成立就跳转到另外一个地址执行

当前循环体为空,意味着循环速度很快,由于CPU访问寄存器的速度远大于访问内存的速度,所以load执行消耗的时间远多于cmp,也就是执行一次load,会执行很多次load

t2线程中是我们要手动修改count的,要知道load是计算机执行的指令,肯定比人要快很多,所以在t2修改之前会执行很多次的load,JVM发现每次load执行的结果都一样就会把load操作优化掉,后续再执行到对应的代码就不再真正load,而是直接读取load过的寄存器中的值了

上述优化的初衷是为了让程序执行的速度更快,但在多线程这里反而引起了bug

在上述代码中添加一个IO操作或者阻塞操作,循环速度就会大幅降低,也就不会优化掉load,IO操作是不会被优化的:

public static int count = 0;
Thread t1 = new Thread(() -> {while (count == 0) {System.out.println("执行IO操作");}
});

总结:上述问题本质是编译器/JVM优化引起的,一个线程对共享变量的修改可能不会被其他线程立即看到,导致其他线程读取到的可能是旧值,从而引发线程安全问题。这就是内存可见性问题

那么该如何解决该问题

1.2 volatile 关键字

给变量加上volatile关键字后,编译器就不会触发上述优化

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

注意:volatile只能保证内存可见性,并不能保证操作的原子性

public class Demo {private static volatile int count = 0;public static void main(String[] args) throws InterruptedException {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 = "+ count);}}

二、线程等待通知机制

2.1 引入概念

先看下面一个ATM取钱的场景

此时小新正在取钱,发现ATM机中钱不够,于是就开锁出来,接下来就该其他人去取钱,但有可能小新觉得自己操作不对就又进去,出来之后发现又不对于是又进去,像这样某个线程频繁获取释放锁,以至于其他线程分配不到CPU资源的问题称为"线程饿死"

系统中线程调度是无序的,线程饿死的情况就有可能出现,但注意:这并不是死锁,死锁是卡死,而线程饿死只会卡住一下下

线程等待通知机制可以调整线程的执行顺序来解决这个问题,通过添加判断条件判定当前逻辑是否能够执行,如果不能就wait(主动进行阻塞)就把执行的机会让给别的线程了,避免该线程进行无意义的重试

2.2 wait()方法

wait()做的事情:

  • 使当前执行代码的线程进行等待(把线程放在等待队列中)
  • 释放当前的锁
  • 被唤醒时,重新尝试获取这个锁

可以看到wait()做的事情有释放当前的锁,也就是wait()必须放在synchronized里面,否则会抛出异常:

public class Demo {public static void main(String[] args) throws InterruptedException {Object object = new Object();System.out.println("等待之前");object.wait();System.out.println("等待之后");}
}

正确的写法如下:

public class Demo {public static void main(String[] args) throws InterruptedException {Object object = new Object();synchronized (object) {System.out.println("等待之前");object.wait();System.out.println("等待之后");}}
}

当前代码会一直等待下去

wait()结束等待的条件:

  • 其他线程调用该对象的notify()
  • wait等待时间超时(和sleep(1000)效果类似,wait(1000),就是等待1s后如果没有被唤醒就自动唤醒)
  • 其他线程调⽤该等待线程的interrupted⽅法,导致wait抛出InterruptedException异常
     

2.3 notify()方法

notify 方法用来唤醒等待线程的

  • 该⽅法是⽤来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁
  • 如果有多个线程等待,则有线程调度器随机挑选出⼀个呈wait状态的线程(并没有"先来后到")

看如下示例:

public class Demo {public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(() -> {synchronized (locker) {System.out.println("t1等待之前");try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t1等待之后");}});Thread t2 = new Thread(() -> {synchronized (locker) {System.out.println("t2等待之前");locker.notify();System.out.println("t2等待之后");}});t1.start();t2.start();}}

打印结果:

注意:t2线程的notify()执行完后,并不会释放锁,而是代码走出synchronized后才会真正把锁释放t1线程拿到锁之后继续执行,因此肯定先打印t2等待之后,后打印t1等待之后

2.4 notifyAll() 方法

notifyAll 可以一次唤醒所有的等待线程

public class Demo {public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(() -> {synchronized (locker) {System.out.println("t1等待之前");try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t1等待之后");}});Thread t2 = new Thread(() -> {synchronized (locker) {System.out.println("t2等待之前");try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t2等待之后");}});Thread t3 = new Thread(() -> {synchronized (locker) {System.out.println("t3等待之前");locker.notifyAll();System.out.println("t3等待之后");}});t1.start();t2.start();Thread.sleep(1000);t3.start();}}

在t3.start()方法之前放一个sleep是为了防止t3先执行了notify,此时t1或t2还没有wait,此时直接notify没有任何效果,也不会抛异常,放一个sleep是为了保证先wait再notify

接下来看代码的执行效果

所有线程都执行结束,如果改为notify方法,再看代码的执行效果

由于notify会随机唤醒一个等待线程,这里唤醒的t1,此时t3没有被唤醒也就不会尝试获取锁,没有锁就不会继续执行接下来的逻辑,所以t3一直处于等待

如果不想让t3一直等下去,就将t3的wait改为带有时间版本的,这样时间一到就会自动被唤醒

2.5 面试题:wait() 和 sleep()的区别

  1. wait必须搭配 synchronized 来使用,否则会抛出IllegalMonitorStateException 异常,而 sleep可以在任何地方使用
  2. wait是Object类的一个普通方法,sleep是Thread类的一个静态方法
  3. 线程可以等 sleep 中的计时结束后主动唤醒,但如果是无参版本的 wait,则需要等其他线程调用 notify 或 notifyAll 来被动唤醒
  4. 调用 sleep 方法线程会进入 TIMED_WAITING 有时限等待状态,而调用无参数的 wait 方法,线程会进入 WAITING 无时限等待状态

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

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

相关文章

(0基础保姆教程)-JavaEE开课啦!--12课程(Spring MVC注解 + Vue2.0 + Mybatis)-实验10

一、常见的SpringMVC注解有哪些&#xff1f; 1.Controller&#xff1a;用于声明一个类为 Spring MVC 控制器。 2.RequestMapping&#xff1a;用于将 HTTP 请求映射到特定的处理方法上。可以指定请求类型&#xff08;GET、POST等&#xff09;和URL路径。 3.GetMapping&#xff…

20241124 Typecho 视频插入插件

博文免不了涉及到视频插入这些,网上的插件都或多或少的比较重,和Typecho的风格不搭配 后面就有了DPlay插件精简而来的VideoInsertion插件 VideoInsertion: Typecho 视频插入插件 目录结构 rockhinlink-ht2:/var/www/html/typecho/usr/plugins/VideoInsertion$ tree -h [4.…

网络地址转换

NAT概述 解决公有地址不足&#xff0c;并且分配不均匀的问题 公有地址&#xff1a;由专门的机构管理、分配&#xff0c;可以在因特网上直接通信 私有地址&#xff1a;组织和个人可以任意使用&#xff0c;只能在内网使用的IP地址 A、B、C类地址中各预留了一些私有IP地址 A&…

H5流媒体播放器EasyPlayer.js网页直播/点播播放器如果H.265视频在播放器上播放不流畅,可以考虑的解决方案

随着流媒体技术的迅速发展&#xff0c;H5流媒体播放器已成为现代网络视频播放的重要工具。其中&#xff0c;EasyPlayer.js网页直播/点播播放器作为一款功能强大的H5播放器&#xff0c;凭借其全面的协议支持、多种解码方式以及跨平台兼容性&#xff0c;赢得了广泛的关注和应用。…

以达梦为数据库底座时部署的微服务页面报乱码,调整兼容模式

1.问题描述 部署微服务&#xff0c;文件、代码是延用的mysql类型的&#xff0c;部署前做了部分适配&#xff0c;但是在使用dm数据库进行安装的服务在页面上查询出的数据却都是乱码 2.查询官网&#xff0c;注意到一个参数COMPATIBLE_MODE兼容模式的配置 考虑是延用mysql&…

【RL Base】强化学习核心算法:深度Q网络(DQN)算法

&#x1f4e2;本篇文章是博主强化学习&#xff08;RL&#xff09;领域学习时&#xff0c;用于个人学习、研究或者欣赏使用&#xff0c;并基于博主对相关等领域的一些理解而记录的学习摘录和笔记&#xff0c;若有不当和侵权之处&#xff0c;指出后将会立即改正&#xff0c;还望谅…

Spring Boot【三】

自动注入 xml中可以在bean元素中通过autowire属性来设置自动注入的方式&#xff1a; <bean id"" class"" autowire"byType|byName|constructor|default" /> byName&#xff1a;按照名称进行注入 byType&#xff1a;按类型进行注入 constr…

软件报错:找不到vcomp140.dll的原因分析,总结六种解决vcomp140.dll的方法

vcomp140.dll是一个与MicrosoftVisualCRedistributableforVisualStudio2015相关的动态链接库文件&#xff0c;主要用于支持并行编程。这个DLL文件是VisualC库的一部分&#xff0c;用来处理并行计算&#xff0c;特别是那些利用OpenMP(OpenMulti-Processing)技术编写的程序。分析…

android 项目多电脑共用github及github项目迁移

背景&#xff1a;最新需要将公司的项目在本地电脑进行使用&#xff0c;将项目迁移到本地电脑。 操作步骤&#xff1a; ssh 公钥绑定github上 : https://blog.csdn.net/mo_sss/article/details/137910910 用github进行克隆时无法下载&#xff08;已将本地创建的公钥上传gith…

常用元器件使用方法18:单节锂电池充电管理芯片XT4052的使用方法

文章目录 一、产品概述二、产品特点三、典型应用电路图四、引脚分配五、应用电路六、PCB应用建议一、产品概述 XT4052 是一个完善的单片锂离子电池恒流/恒压线形电源管理芯片。它薄的尺寸和小的外包装使它便于便携应用。更值得一提的是,XT4052专门设计适用于USB的供电规格。得…

java八股-分布式服务的接口幂等性如何设计?

文章目录 接口幂等token Redis分布式锁 原文视频链接&#xff1a;讲解的流程特别清晰&#xff0c;易懂&#xff0c;收获巨大 【新版Java面试专题视频教程&#xff0c;java八股文面试全套真题深度详解&#xff08;含大厂高频面试真题&#xff09;】 https://www.bilibili.com/…

python的字体如何调整

首先打开pycharm&#xff0c;新建一个Python文件&#xff0c;点击“File”&#xff0c;在下拉菜单栏中选择“New”。 然后点击“File”&#xff0c;输入我们的Python文件名称&#xff0c;后缀一定要填写“.py”&#xff0c;要不会出错&#xff0c;单击“OK”。 在新文件中输入代…

【Linux系列】Chrony时间同步服务器搭建完整指南

1. 简介 Chrony是一个用于Linux系统的高效、精准的时间同步工具&#xff0c;通常用于替代传统的NTP&#xff08;Network Time Protocol&#xff09;服务。Chrony不仅在系统启动时提供快速的时间同步&#xff0c;还能在时钟漂移较大的情况下进行及时调整&#xff0c;因此广泛应…

Ubuntu问题 -- 使用scp将本机文件传输至ubuntu服务器中

目的 临时没有文件传输工具使用一条命令快速传输指定文件或文件夹 使用scp命令 传输指定文件 scp -P 22 D:\Storage\myCache\UE\Linux_ue_demo.zip txl10.1.112.93:/home/txl-P是远程机器的ssh端口号, SCP&#xff08;安全复制协议&#xff09;使用和SSH&#xff08;安全外壳…

Web 毕设篇-适合小白、初级入门练手的 Spring Boot Web 毕业设计项目:电影院后台管理系统(前后端源码 + 数据库 sql 脚本)

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 项目介绍 2.0 用户登录功能 3.0 用户管理功能 4.0 影院管理功能 5.0 电影管理功能 6.0 影厅管理功能 7.0 电影排片管理功能 8.0 用户评论管理功能 9.0 用户购票功…

PHP 函数的未来发展有哪些变化呢

PHP 8.0 引入了一些新特性&#xff0c;比如 JIT 编译器、联合类型、nullsafe 运算符等。 JIT 编译器 (Just-In-Time Compiler)&#xff1a;PHP 8.0 引入了实验性的 JIT 编译器&#xff0c;可以显著提高代码执行速度。联合类型&#xff08;Union Types&#xff09;&#xff1a;…

Java文件遍历那些事

文章目录 一、简要概述二、文件遍历几种实现1. java实现2. Apache common-io3. Spring 三、最终结论 一、简要概述 文件遍历基本上是每个编程语言具备的基本功能&#xff0c;Java语言也不例外。下面我们以java原生实现&#xff0c;Apache common-io、spring框架实现为例来比较…

【网络安全设备系列】12、态势感知

0x00 定义&#xff1a; 态势感知&#xff08;Situation Awareness&#xff0c;SA&#xff09;能够检测出超过20大类的云上安全风险&#xff0c;包括DDoS攻击、暴力破解、Web攻击、后门木马、僵尸主机、异常行为、漏洞攻击、命令与控制等。利用大数据分析技术&#xff0c;态势感…

MySQL 启动失败问题分析与解决方案:`mysqld.service failed to run ‘start-pre‘ task`

目录 前言1. 问题背景2. 错误分析2.1 错误信息详解2.2 可能原因 3. 问题排查与解决方案3.1 检查 MySQL 错误日志3.2 验证 MySQL 配置文件3.3 检查文件和目录权限3.4 手动启动 MySQL 服务3.5 修复 systemd 配置文件3.6 验证依赖环境 4. 进一步优化与自动化处理结语 前言 在日常…

企业如何落地搭建商业智能BI系统

随着新一代信息化、数字化技术的应用&#xff0c;引发了新一轮的科技革命&#xff0c;现代化社会和数字化的联系越来越紧密&#xff0c;数据也变成继土地、劳动力、资本、技术之后的第五大生产要素&#xff0c;这一切都表明世界已经找准未来方向&#xff0c;前沿科技也与落地并…