当前位置: 首页 > news >正文

JavaEE初阶第八期:解锁多线程,从 “单车道” 到 “高速公路” 的编程升级(六)

专栏:JavaEE初阶起飞计划

个人主页:手握风云

目录

一、volatile关键字

1.1. 原理

1.2. Java memory model(Java内存模型)

1.3. volatile与synchronized的区别

二、wait和notify

2.1. wait()方法

2.2. notify()方法

2.3. notifyAll()方法


一、volatile关键字

1.1. 原理

        当给变量添加了volatile关键字后,当编译器看到volatile的时候,就会提醒JVM运行的时候不进行上述的优化。具体来说,在读写volatile变量的前后指令添加“内存屏障相关的指令”。

1.2. Java memory model(Java内存模型)

        首先一个Java进程,会有一个“主内存”存储空间,每个Java线程又会有自己的“工作内存”存储空间。形如下面的代码,t1进行flag变量的判定,就会把flag值从主内存先读取到工作内存,用工内存中的值进行判定。同时t2对flag进行修改,修改的则是主内存的值,主内存中的值不会影响到工作内存中的值。这里的工作内存相当于是打了个比方,本质上是CPU的寄存器和CPU的缓存构成的统称。

import java.util.Scanner;public class Demo1 {private static int flag = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (flag == 0) {}System.out.println("t1线程结束");});Thread t2 = new Thread(() -> {Scanner in = new Scanner(System.in);System.out.println("请输入flag的值:");flag = in.nextInt();System.out.println("t2线程结束");});t1.start();t2.start();t1.join();t2.join();}
}

        其实,存储数据,不光是只有内存,外存(硬盘)、CPU寄存器、CPU上的缓存。

        上图中的缓存也是CPU上存储数据的单元。寄存器能存数据,但是空间小;内存能存数据,空间大,但是速度慢。为了能够更好地协调寄存器和内存的数据同步,因此现代CPU都引入了缓存。CPU的缓存,空间比寄存器要大,速度比内存快。

        上图中,越往上,速度就越快,空间就越小,成本就越高。编译器优化,把本身从内存读取的值,优化成从寄存器或者L1缓存、L2缓存、L3缓存中读取。

        编译器优化,并非是100%触发,根据不同的代码结构,可能产生出不同的优化效果。形如下面的代码

import java.util.Scanner;public class Demo2 {private static int flag = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (flag == 0) {try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1线程结束");});Thread t2 = new Thread(() -> {Scanner in = new Scanner(System.in);System.out.println("请输入flag的值:");flag = in.nextInt();System.out.println("t2线程结束");});t1.start();t2.start();t1.join();t2.join();}
}

        虽然没写volatile,但是加了sleep也不会触发上述优化:1. 循环速度大幅度降低了;2. 有了sleep之后,一次循环的瓶颈就不是load,在于sleep上,此时优化也没什么用;3. sleep本身会触发线程调度,调度过程触发上下文切换,再次加载也会触发这个值重新读取了。

        如下代码,我们改为一个静态成员变量count,会发现count也会触发优化。

import java.util.Scanner;public class Demo2 {private static int flag = 0;private static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (flag == 0) {count++;}System.out.println("t1线程结束");});Thread t2 = new Thread(() -> {Scanner in = new Scanner(System.in);System.out.println("请输入flag的值:");flag = in.nextInt();System.out.println("t2线程结束");});t1.start();t2.start();t1.join();t2.join();}
}

1.3. volatile与synchronized的区别

public class Demo3 {private static volatile int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 50_000; i++) {count++;}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50_000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

        volatile这个关键字,能够解决内存可见性引起的线程安全问题,但是不具备原子性这样的特点。synchronized和volatile是两个不同的维度,synchronized是针对两个线程进行修改,而volatile是读取一个线程,另一个修改。

二、wait和notify

        因为线程调度的顺序是不确定的,那我们就得保证每一种可能下都是正确的,就有点难搞了。我们之前提到过,join()方法可以控制线程的结束顺序。两个线程在运行的时候,我们希望是持续执行下去,但是两个线程中的某些环节,我们希望是能够有一定的顺序。

        例如,假设有线程1和线程2。我们希望线程1先执行完某个逻辑后,再让线程2执行,此时就可以让线程2通过wait()主动进行阻塞,让线程1先参与调度。等线程1执行完对应的逻辑后,就可以通过notify()唤醒线程2。

        另外wait和notify也能解决线程饿死的问题。线程饿死指的是在多线程编程中,某个或某些线程由于长时间无法获取到执行所需的资源,导致其任务迟迟无法完成,甚至永远无法执行的情况。 简而言之,就是某个线程“饿着肚子”等了很久,但一直没能得到“食物”。

        如下图所示,当一个滑稽老铁进入ATM机里面取钱时,会进行上锁,其他滑稽老铁就必须在外面阻塞等待。当先进去的滑稽老铁发现ATM机里面没钱,便出去,而后又怀疑自己是不是看错了,于是又再次进入ATM机……如此循环往复,造成其他线程无法去CPU上执行,导致线程饿死。

        线程饿死不像死锁那么严重。死锁发生之后,就会僵持住,除非程序重启,否则一直卡住。线程饿死,其他线程还是有一定机会拿到锁的,只是拿到锁的时间会延迟,降低程序的效率。

        注意:wait、notify、notifyAll都是Object类里的方法。Java中任何一个类,都会有上述三种方法。

2.1. wait()方法

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

        在Java标准库中,但凡涉及到阻塞类的方法,都有可能抛出InterruptedException异常,所以我们这里也要在前面加上InterruptedException异常。

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

        但我们一运行程序之后,在打印“wait之前”语句后出现了IllegalMonitorStateException异常。此处的Monitor指的是sychronized,因为sychronized在JVM的底层实现被称为“监视器锁”。上面的异常是指锁的状态不符合预期。wait内部的第一件事就是释放锁,但释放锁的前提是得先拿到锁。像前面提到的滑稽老铁发现ATM机里面没有钱,如果滑稽老铁在里面等,意味着一直持有这个锁,其他人进不来。wait方法就要搭配sychronized使用。

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

        此处的阻塞会持续进行,直到其他线程调用notify。

import java.util.Scanner;public class Demo5 {private static Object locker = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {System.out.println("t1等待之前");synchronized (locker) {try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1等到之后");});Thread t2 = new Thread(() -> {Scanner in = new Scanner(System.in);System.out.println("请输入任意内容,唤醒t1");in.next();// 必须是同一个锁对象synchronized (locker) {locker.notify();}});t1.start();t2.start();}
}

        wait要做的事情:

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

        使用wait的时候,阻塞其实是有两个阶段的:1. WAITING的阻塞,通过wait等待其他线程的通知;2. BLOCKED的阻塞,当收到通知之后,就会重新获取锁,可能又会遇到锁竞争。假设notify后面还有别的逻辑,此时锁就会多占用一会儿。

        默认情况下,wait的阻塞是死等。wait也可以设置参数等待时间的上限。

import java.util.Scanner;/*** @author gao* @date 2025/7/9 20:54*/public class Demo6 {private static Object locker = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {System.out.println("t1等待之前");synchronized (locker) {try {// t1在1000ms内没有收到任何通知,就会自动唤醒locker.wait(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1等待之后");});Thread t2 = new Thread(() -> {Scanner in = new Scanner(System.in);System.out.println("请输入任何内容,唤醒t1");in.next();synchronized (locker) {locker.notify();}});t1.start();t2.start();}
}

2.2. notify()方法

import java.util.Scanner;public class Demo7 {public static Object locker = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {System.out.println("t1等待之前");synchronized (locker) {try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1等待之后");});Thread t2 = new Thread(() -> {System.out.println("t2等待之前");synchronized (locker) {try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t2等待之后");});Thread t3 = new Thread(() -> {System.out.println("t3等待之前");synchronized (locker) {try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t3等待之后");});Thread t4 = new Thread(() -> {Scanner in = new Scanner(System.in);System.out.println("输入任意内容,唤醒一个线程:");in.next();synchronized (locker) {locker.notify();}});t1.start();t2.start();t3.start();t4.start();}
}

        这里我们就唤醒了t3线程,而其他两个线程还在阻塞等待。我们多运行几次,结果也会不同。注意,这里操作系统的随机调度并不是概率论里的概率均等,而是无法预测的。在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

2.3. notifyAll()方法

        使⽤notifyAll⽅法可以⼀次唤醒所有的等待线程。

import java.util.Scanner;public class Demo8 {private static Object locker = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {System.out.println("t1等待之前");synchronized (locker) {try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1等待之后");});Thread t2 = new Thread(() -> {System.out.println("t2等待之前");synchronized (locker) {try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t2等待之后");});Thread t3 = new Thread(() -> {System.out.println("t3等待之前");synchronized (locker) {try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t3等待之后");});Thread t4 = new Thread(() -> {Scanner in = new Scanner(System.in);System.out.println("输入任意内容,唤醒一个线程:");in.next();synchronized (locker) {locker.notifyAll();}});t1.start();t2.start();t3.start();t4.start();}
}

        如果没有任何对象在wait,凭空调用notify或者notifyAll也不会有任何副作用。

http://www.lryc.cn/news/583247.html

相关文章:

  • 软件设计师中级概念题
  • Selenium+Pytest自动化测试框架实战前言#
  • 汽车工业制造领域与数字孪生技术的关联性研究​
  • Microsoft AZ-305 Exam Question
  • 迁移Oracle SH 示例 schema 到 PostgreSQL
  • 亚马逊广告进阶指南:长尾词应如何去挖掘
  • RapidRAW RAW 图像编辑器
  • 游戏开发学习记录
  • 码云创建分支
  • 分库分表之实战-sharding-JDBC绑定表配置实战
  • 掌握PDF转CAD技巧,提升工程设计效率
  • 模型内部进行特征提取时,除了“减法”之外,还有哪些技术
  • Android ttyS2无法打开该如何配置 + ttyS0和ttyS1可以
  • BEV感知算法:自动驾驶的“上帝视角“革命
  • c语言学习_函数递归2
  • 深度学习模型在C++平台的部署
  • Spring Boot微服务中集成gRPC实践经验分享
  • 1️⃣理解大语言模型
  • 百度文心一言开源ERNIE-4.5深度测评报告:技术架构解读与性能对比
  • Shell 脚本0基础教学(一)
  • 【计算机组成原理——知识点总结】-(总线与输入输出设备)-学习笔记总结-复习用
  • Energy-Based Transformers:实现通用系统2思维的新范式
  • HOOPS Communicator 2025.5.0版本更新速览:性能、测量与UI全面优化
  • C++入门基础篇(一)
  • 《【第五篇】图片处理自动化:让你的视觉内容更专业!:图片处理基础与批量裁剪》
  • Unity Demo-3DFarm详解-其二
  • 极简相册管理ios app Tech Support
  • 无人机报警器频段模块设计与运行要点
  • Excel 常用高级用法
  • 使用LLaMA-Factory微调Qwen2.5-VL-3B 的目标检测任务-使用LLaMA-Factory webui进行训练