【JavaEE】多线程(五)- 基础知识完结篇

多线程(五)

文章目录

  • 多线程(五)
    • volatile关键字
      • 保证内存可见性
        • JMM(Java Memory Model)
      • 不保证原子性
    • wait 和 notify
      • wait()
      • notify()
      • 线程饿死

上文我们主要讲了 synchronized以及线程安全的一些话题

可重入锁 => 死锁

  1. 一个线程,一把锁,连续加锁两次
  2. 两个线程两把锁
  3. N个线程N把锁,哲学家就餐问题♂

产生死锁的四个必要条件

  1. 互斥使用
  2. 不可抢占/剥夺
  3. 请求和保持 获取多把锁 获取第二把锁的时候 第一把锁不要释放
  4. 循环等待/环路等待

续上文,本篇我们继续聊多线程~

volatile关键字

保证内存可见性

计算机运行的代码/程序,经常要访问数据,这些依赖的数据,往往就存储在内存中。(也就是定义一个变量,变量就是存储在内存中)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

cpu使用这个变量的时候,就会把这个内存数据,先读出来,放到cpu寄存器里面,在参与运算load

这里我们要注意:

  • cpu的读取内存操作,其实是非常慢的
  • cpu进行大部分操作都是很快的,但是一旦操作读/写内存,此时速度就会慢下来
  • 读内存 相比于 读硬盘,快几千倍,上万倍
  • 读寄存器,相比于读内存,又快了几千倍,上万倍

因此,为了解决上述问题,提高效率,此时编译器就可能对代码做出优化,把一些本来要读内存的操作,优化成读寄存器,减少读内存的次数,也就可以提高整体程序的效率了

见以下代码:

//多线程引起  bug
public class Demo19 {private static int isQuit = 0;public static void main(String[] args) {Thread t1 = new Thread(()->{while (isQuit ==0){//循环体里啥都没干//此时意味着这个循环,一秒钟会执行很多次}System.out.println("t1 退出");});t1.start();Thread t2 = new Thread(()->{System.out.println("请输入 isQuit :>");Scanner scanner = new Scanner(System.in);//一旦用户输入的值,不为0,此时就会使t1线程结束isQuit = scanner.nextInt();});t2.start();}
}

这段代码我们的预期是:用户输入非 0 值之后,t1线程要退出~

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

但是当我们输入非 0 值之后,此时的t1线程并没有退出

我们可以通过jconsole来看看它此时的运行状态

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

很明显,实际效果和预期效果不一样。
这是由于多线程引起的bug.也是线程安全问题!!

之前是两个线程,同时修改同一个变量,现在是一个线程读,一个线程修改,也可能会有问题。

此处问题,实际上就是内存可见性情况引起的~

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

编译器的优化,初心其实是好的,希望能够提高程序的效率,但是优化错咯。因为提高效率的前提是要保证逻辑不变,但是此时由于修改isQuit代码是另外一个线程的操作, 编译器没有正确的判定,所以编译器以为没人修改isQuit,就做出上述的优化,也就导致bug了~

此时解决方案就是:volatile

在多线程环境下,编译器对于是否要进行这样的优化,判定不一定准,就需要我们通过volatile关键字,告诉编译器,你不要优化!(优化,是算的快了,但是算的不准了)

public class Demo20 {private volatile static int isQuit = 0;public static void main(String[] args) {Thread t1 = new Thread(()->{while (isQuit ==0){//循环体里啥都没干//此时意味着这个循环,一秒钟会执行很多次}System.out.println("t1 退出");});t1.start();Thread t2 = new Thread(()->{System.out.println("请输入 isQuit :>");Scanner scanner = new Scanner(System.in);//一旦用户输入的值,不为0,此时就会使t1线程结束isQuit = scanner.nextInt();});t2.start();}
}

在这里插入图片描述

不过

public class Demo19 {private static int isQuit = 0;public static void main(String[] args) {Thread t1 = new Thread(()->{while (isQuit ==0){//循环体里啥都没干//此时意味着这个循环,一秒钟会执行很多次try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1 退出");});t1.start();Thread t2 = new Thread(()->{System.out.println("请输入 isQuit :>");Scanner scanner = new Scanner(System.in);//一旦用户输入的值,不为0,此时就会使t1线程结束isQuit = scanner.nextInt();});t2.start();}
}

此时没加volatile,但是给循环里加了个sleep
此时,t1线程是可以顺利退出的!
加了sleep之后,while循环执行速度就慢了.
由于次数少了,load操作的开销,就不大了.
因此,优化也就没必要进行了.
没有触发load的优化,也就没有触发内存可见性问题了.
到底啥时候代码有优化,啥时候没有?也说不清~~
使用volatile是更靠谱的选择


这里稍微总结一下:

内存可见性也是属于一种线程安全的情况。

这都是编译器进行代码优化搞出来的bug,代码优化是非常普遍的情况,编译器为了进一步提高代码的执行效率,会在保持逻辑不变的情况下,调整生成代码的内容。

但是如果是多线程的代码,代码优化就有可能会出现误判,优化之后的代码逻辑和之前的就不一样了~


其次,关于内存可见性,还涉及到一个关键概念

JMM(Java Memory Model)

Java内存模型 -> Java规范文档的叫法

JMM主要关注以下几个方面:

  1. 可见性(Visibility):保证一个线程对共享变量的修改对其他线程是可见的。当一个线程修改了一个共享变量的值后,其它线程能够看到这个修改。
  2. 原子性(Atomicity):保证对于一个共享变量的读写操作是原子性的,不会出现中间状态。
  3. 有序性(Ordering):保证程序执行的结果与源代码的顺序一致。对于一段代码的执行,可能会进行指令重排序优化,但是不能改变执行结果的顺序。

JMM使用了一些机制来实现这些特性,如内存屏障(Memory Barrier)、volatile关键字、锁、synchronized等。这些机制帮助Java编译器和运行时环境协同工作,以保证多线程程序的正确性。

理解JMM对于编写正确且高效的多线程程序非常重要。遵循JMM的规则可以避免在多线程程序中出现各种内存可见性、原子性和有序性的问题。

总结来说,JMM定义了Java程序在多线程环境下共享变量的访问规则,保证了多线程程序的正确性和可预测性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

volatilesynchronized都能对线程安全起到一定的积极作用,但是他们也是各司其职的,volatitl是不能保障原子性的~

volatilesynchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.

不保证原子性

看下面例子:

public class VolatileExample {private static volatile int counter = 0;public static void main(String[] args) {new Thread(() -> {for (int i = 0; i < 1000; i++) {counter++;}}).start();new Thread(() -> {for (int i = 0; i < 1000; i++) {counter++;}}).start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Counter: " + counter);}
}

在上面的例子中,我们有两个线程对 counter 变量进行递增操作。counter 被声明为 volatile,所以每个线程都能够立即看到对 counter 的修改。

但是,由于 counter++ 不是一个原子操作,而是由读取变量、加1、写回变量三个步骤组成。在多线程环境下运行时,一个线程对 counter 的修改可能被另一个线程打断,导致数据不一致的问题。

比如,一个线程读取了 counter 变量的值为10,准备将其加1变为11,但这时被另一个线程打断,修改为11的 counter 写回变为10,然后再将其加1变为11。

由于 volatile 不能保证多个线程同时对同一个变量进行原子操作,所以在上面的代码中,最终打印的结果可能会小于预期的2000。

如果需要保证变量的原子性,可以使用原子类(比如 AtomicInteger)或加锁机制(比如 synchronizedLock)。这些机制能够确保对变量的修改是原子性的,从而避免了竞态条件和数据不一致性的问题。

总结来说,虽然 volatile 关键字可以保证变量的可见性和禁止指令重排序,但它并不能提供变量操作的原子性。如果需要保证原子性,应该使用原子类或加锁机制。


wait 和 notify

多线程中比较重要的机制~是用来协调多个线程的执行顺序

因为本身多个线程的执行顺序是随机的(系统随机调度,抢占式执行的)

所以很多时候,我们希望能够通过一定的手段,协调的执行顺序。

比如说join,它是影响到线程结束的先后顺序,但是相比之下,此处是希望线程不结束,也能够有先后顺序的控制。

wait:等待,让指定线程进入阻塞状态

notify:通知,唤醒对应的阻塞状态的线程


join等待的过程和“主线程”没有直接的联系,哪个线程调用join哪个线程就阻塞。

public class Demo18 {public static void main(String[] args) {Thread t1 = new Thread(()->{for (int i = 0; i < 5; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1 结束!");});Thread t2 = new Thread(()->{for (int i = 0; i < 5; i++) {try {t1.join();Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t2 结束!");});t1.start();t2.start();System.out.println("主线程结束!");}
}

waitnotify都是Object的方法

随便定义一个对象都可以wait notify

wait()

我们先给一个示例代码:

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

然而这里会报错:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

IllegalMonitorStateException非法的 监视器 异常

而什么是监视器呢?

synchronized:也叫做监视器锁

wait 在执行要做的三件事情:

公平,公平,还是他妈的公平!(buhsi)

  • 释放当前的锁

  • 让线程进入阻塞

  • 当线程被唤醒, 重新尝试获取这个锁.

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

修改代码:

public class Demo19 {public static void main(String[] args) throws InterruptedException {Object object = new Object();synchronized (object) {System.out.println("wait 之前");//把 wait 放入 synchronized 里面来调用,保证确实是拿到锁object.wait();// wait 会持续地阻塞等待下去,直到其他线程调用 notify 唤醒System.out.println("wait 之后");}}
}

所以这串的代码的wait,就会持续等待,直到其他线程调用notify唤醒

在这里插入图片描述


wait除了默认的无参数版本之外,还有一个带参数的版本.
带参数的版本就是指定超时时间,
避免wait无休止的等待下去

notify()

先看示例代码:

// notify 唤醒
public class Demo20 {public static void main(String[] args) {Object object = new Object();Thread t1 = new Thread(()->{synchronized (object){System.out.println(" wait 之前");try {object.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(" wait 之后");}});Thread t2 = new Thread(()->{try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (object){System.out.println(" 进行通知 ");object.notify();}});t1.start();t2.start();}
}
  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。

  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)

  • notify()方法后,当前线程不会马上释放该对象锁,要等到执行~方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。


线程饿死

使用wait notify可以避免线程饿死~

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

针对上述情况,同样也可以使用wait notify来解决

可以让1号loopy,在发现没钱的时候,就进行waitwait内部本身就会释放锁,并且进入阻塞)

那么1号loopy就不会参与后续的竞争了,也把锁释放出来让别人取,就给其他的loopy提供了机会~

wait的过程是等,等待运钞车将钱送过来,运钞车的线程就相当于调用notify唤醒的线程,这个等的状态时阻塞的,什么都不做,也就不会占据cpu


当线程调用了一个对象的 wait 方法时,它进入了该对象的等待集(wait set),并释放了持有的锁。

在这里,我们假设有多个线程都在等待这个对象上。

  • 当另一个线程调用了相同对象的 notify 方法时,它会随机选择一个线程,从等待集中唤醒一个线程,使其从等待状态转移到可运行状态。被唤醒的线程会重新尝试获取锁,并从 wait 方法返回继续执行。

  • notifyAll 方法则会唤醒所有在等待集中的线程,使它们从等待状态转移到可运行状态。每个被唤醒的线程都会尝试重新获取锁,并从 wait 方法返回继续执行。

    在唤醒的时候,wait要涉及一个重新获取锁的过程,也是需要串行执行的。

这种等待和唤醒的机制通常用于线程间的协作和同步。例如,当一个线程需要等待某个条件满足时,它可以调用对象的 wait 方法,而其他线程则可以在某个条件满足时调用 notifynotifyAll 方法来唤醒等待的线程。

需要注意的是,waitnotifynotifyAll 都必须在同步代码块(synchronized)或同步方法中使用,以确保线程的安全性和正确性。

因此,综上,虽然提供了notifyAll,但是相比之下notify更可控,使用的频率高一些。


至此,多线程的基础知识就介绍到这里,接下来会详细聊聊多进程的进阶,敬请期待~

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

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

相关文章

故障注入常用方法有哪些 其重要性是什么

故障注入是一种有效的测试方法&#xff0c;可用于评估系统对异常情况的响应。通过这种测试方法&#xff0c;可以发现系统中的潜在问题&#xff0c;并采取适当措施来改进系统的质量和性能。本文将介绍故障注入常用方法及重要性! 一、故障注入常用方法 1、随机故障注入&#xff1…

《向量数据库指南》——向量数据库 有必要走向专业化吗?

向量数据库 有必要走向专业化吗? 向量数据库系统的诞生,来源于具体业务需求——想要高效处理海量的向量数据,就需要更细分、更专业的数据基础设施,为向量构建专门的数据库处理系统。 但这种路径是必须的吗? 从产品层面讲,如果传统数据库厂商不单独研发向量数据库,那么…

Postgresql中的C/C++混编(JIT)

1 Postgresql编译JIT 整体上看使用了GCC、G编译文件&#xff0c;最后用G汇总&#xff1a; GCC编译的三个.o文件llvmjit、llvmjit_deform、llvmjit_expr llvmjit.c -> llvmjit.o gcc -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -…

Unity布料系统Cloth

Unity布料系统Cloth 介绍布料系统Cloth(Unity组件)组件上的一些属性布料系统的使用布料约束Select面板Paint面板Gradient Tool面板 布料碰撞布料碰撞碰撞适用 介绍 布料系统我第一次用是做人物的裙摆自然飘动&#xff0c;当时我用的是UnityChan这个unity官方自带的插件做的裙摆…

javaee ssm框架项目整合thymeleaf2.0 更多thymeleaf标签用法 项目结构图

创建ssmthymeleaf项目 创建ssmthymeleaf项目参考此文 thymeleaf更多常用标签 <!DOCTYPE html> <html lang"en" xmlns:th"http://www.thymeleaf.org"> <head><meta charset"UTF-8"><title>Title</title> …

【ccf-csp题解】第7次csp认证-第二题-俄罗斯方块-简单碰撞检测算法

题目描述 思路讲解 本题的主要思路是实现一个draw函数&#xff0c;这个函数可以绘制每一个状态的画布。然后从第一个状态往后遍历&#xff0c;当绘制到某一个状态发生碰撞时&#xff0c;答案就是上一个状态的画布。 此处的状态x实际就是在原来的15*10画布上的第x行开始画我们…

你必须知道的数据查询途径!!

在当今信息爆炸的时代&#xff0c;我们每天都会面临海量的数据和信息。如何在这些繁杂的信息中快速、准确地找到自己需要的内容&#xff0c;也是当代一个非常重要的技能。下面&#xff0c;我将介绍几种你必须知道的企业数据信息查找途径。 ​ 1. 搜索引擎 搜索引擎是我们日常中…

基于SSM的医用物理学实验考核系统设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

算法题:柠檬水找零(典型的贪心算法问题)

这道题就是纯贪心算法题&#xff0c;遍历每个顾客&#xff0c;先把钱收了&#xff0c;如果是10块钱就判断手里头有没有5元用于找零&#xff1b;如果是20块钱&#xff0c;先判断是不是有10元5元&#xff0c;如果没有就再判断是否有3个5元。没有的话就直接返回 False。(完整题目附…

vue2项目中使用element ui组件库的table,制作表格,改表格的背景颜色为透明的

el-table背景颜色变成透明_el-table背景透明_讲礼貌的博客-CSDN博客 之前是白色的&#xff0c;现在变透明了&#xff0c;背景颜色是蓝色

加密市场波动:地缘政治与美股走弱引发不确定性!

伴随着国庆假期的结束&#xff0c;多日波动率维持低位的加密市场也似乎开始苏醒。近期多次突破28000美元未果的比特币&#xff0c;于9日15:00开始从27800美元附近下跌&#xff0c;最低跌至27260美元&#xff0c;同期以太坊也至1550美元左右&#xff0c;创近半个月来新低。 Coin…

vue接入高德地图获取经纬度

&#x1f90d;step1:高德地图开放平台&#xff0c;根据指引注册成为高德开放平台开发者&#xff0c;并申请 web 平台&#xff08;JS API&#xff09;的 key 和安全密钥; &#x1f90d;step2:在html引入安全密钥&#xff08;获取经纬度用&#xff0c;不然会报错&#xff09; <…

基于SpringBoot的大型商场应急预案管理系统

目录 前言 一、技术栈 二、系统功能介绍 员工信息管理 预案信息管理 预案类型统计 事件类型管理 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍…

手撕各种排序

> 作者简介&#xff1a;დ旧言~&#xff0c;目前大一&#xff0c;现在学习Java&#xff0c;c&#xff0c;c&#xff0c;Python等 > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 目标&#xff1a;掌握每种排序的方法&#xff0c;理解每种排序利弊…

springboot-配置文件优先级

官方文档 https://docs.spring.io/spring-boot/docs/2.7.16/reference/htmlsingle/#features.external-config Spring Boot允许外部化配置&#xff0c;这样就可以在不同的环境中使用相同的应用程序代码。您可以使用各种外部配置源&#xff0c;包括Java属性文件、YAML文件、环境…

ROS 工作空间及功能包

ROS工作空间&#xff08;workspace&#xff09;是一个存放工程开发相关文件的文件夹。 1. 什么是ROS的工作空间 使用ROS实现机器人开发的主要手段是写代码&#xff0c;这些代码文件存放的空间就是工作空间。 工作空间&#xff08;workspace&#xff09;是一个用于存放工程开发…

Spring Data Redis使用方式

1.导入Spring Data Redis的maven坐标 pom.xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> 2. 配置Redis数据源 2.1application.yml文件…

【工具】SSH端口转发管理器,专门管理SSH Port Forwarding

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 开源代码看这里&#xff1a;http://xfxuezhang.cn/index.php/archives/1151/ 背景介绍 有时候需要用到ssh的端口转发功能。目前来说&#xff0c;要么是cmd里手敲指令&#xff0c;但每次敲也太麻烦了&#xff1b;或…

JVM命令行监控工具

JVM命令行监控工具 概述 性能诊断是软件工程师在日常工作中需要经常面对和解决的问题&#xff0c;在用户体验至上的今天&#xff0c;解决好应用的性能问题能带来非常大的收益。 Java作为最流行的编程语言之一&#xff0c;其应用性能诊断一直受到业界广泛关注&#xff0c;可能…

游戏缺少dll文件用什么修复?教你多种dll文件修复方法搞定!

在玩游戏的时候&#xff0c;有时候会遇到一些dll文件缺失的问题&#xff0c;导致游戏无法正常运行。这对于广大游戏爱好者来说无疑是一种巨大的打击。但是不要担心&#xff0c;我们总会有方法来解决这个问题。本文将详细介绍几种解决方法&#xff0c;帮助你轻松修复游戏缺少dll…