JavaEE——volatile、wait、notify三个关键字的解释

news2025/1/12 23:11:24

文章目录

  • 一、volatile和内存可见性
    • 1.解释内存可见性问题
    • 2. volatile 的使用与相关问题
  • 二、wait 和 notify
    • 1.wait 方法
    • 2.notify() 方法
    • 3. 关于 notifyAll() 方法
    • 4. wait 和 sleep 之间的简单比较

一、volatile和内存可见性

前面的文章,我们已经提及到了内存可见性问题,这里在对内存可见性进行简单的描述:内存可见性是指,一个线程对共享变量值的修改,可以被其他线程及时的看到。

1.解释内存可见性问题

对于内存可见性问题,我们已经知道,出现问题的原因在与,一个线程针对一个变量进行读取,同时另一个线程针对这个变量进行修改,此时读到的值不一定就是修改后的值。

下面我通过一个简单的代码来展示一下这个问题:

代码示例:

import java.util.Scanner;

class MyCounter{
    int flag = 0;
}
//通过两个线程对一个元素进行读取和修改操作展现问题
public class ThreadDemo {
    public static void main(String[] args) {
        MyCounter myCounter = new MyCounter();

        Thread t1 = new Thread(()->{
            while(myCounter.flag == 0){

            }
            System.out.println("已经跳出 t1 循环");
        });
        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个数字");
            myCounter.flag = scanner.nextInt();
        });

        t1.start();
        t2.start();
    }
}

运行结果:
在这里插入图片描述
可以发现输入数字改变 flag 的值后,代码没有停止运行。此时我们借助 jconsole 工具来看一下:
在这里插入图片描述
如上图所示,在输入数据前,两个线程的状态。
在这里插入图片描述
在输入数据后,t2 线程消失不见,只剩 t1 线程进行死循环。

在这里我们肯定会有一个疑问,不是已经将 flag 的值修改了,当线程 t1 再次获取的时候应该跳出循环,但是为何仍然出现了死循环。

这里使用汇编来理解,大概分为以下两点:

  1. load,将内存中 flag 的值获取到寄存器中
  2. cmp,将寄存器中的值和 0 进行比较,决定下一步如何执行。

上面的两个操作,是循环的一个整体,这个循环的速度极快,大约在 1 秒钟百万次以上。
在 CPU 对寄存器的读取操作上,速度也是要比计算器对内存读取的速度快很多倍,所以,load 操作和 cmp 操作相比,速度会慢非常多。
正是因为上面的种种原因,导致反复 load 到的结果都一样,对此 JVM 做出了一个大胆的决定,不在多次获取 flag 判定没有修改 flag 的值。(这也是编译器优化的一种方式)

2. volatile 的使用与相关问题

  1. volatile 关键字的使用

通过上面的问题的描述,呢么要解决这个问题只能靠程序员手动进行干预。volatile 这个关键字就是干预的关键所在。

给上面代码中的 flag 变量前加上 volition 关键字进行修饰,表达的意思是告诉编译器,这个变量是 “易变” 的,要求编译器每次都要进行读取操作。

代码示例:

import java.util.Scanner;

class MyCounter{
// 添加 volatile 关键字修饰 flag
    volatile int flag = 0;
}

public class ThreadDemo16 {
    public static void main(String[] args) {
        MyCounter myCounter = new MyCounter();

        Thread t1 = new Thread(()->{
            while(myCounter.flag == 0){

            }
            System.out.println("已经跳出 t1 循环");
        });
        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个数字");
            myCounter.flag = scanner.nextInt();
        });

        t1.start();
        t2.start();
    }
}

运行结果:
在这里插入图片描述
注:volatile 关键字只能修饰变量,不能修饰方法。

  1. volatile 关键字不保证原子性

关于这一点,我们用 synchronized 修饰的代码替换后来展示一下。

代码展示:

class Counter{
    //将原先 synchronized 关键字修改的替换成 volatile 关键字
    public volatile int count = 0;
     public void increase(){
            count++;
    }
}

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        //启动两个线程
        t1.start();
        t2.start();

        //让主线程等待线程的执行
        t1.join();
        t2.join();

        System.out.println(counter.count);
    }
}

上面的代码被 synchronized 关键字修改后可以计算出正确的数值 10万
结果展示:
在这里插入图片描述
因此,这样就证明了 volatile 关键字并不能保证变量的原子性

二、wait 和 notify

线程的最大问题,就是抢占式执行,随机调度。对此,线程执行的先后顺序就难以预知,但是在实际开发中,我们需要更加合理的处理线程之间的先后顺序。

假设两个线程需要合作来完成一项工作,需要交叉配合执行。那么,使用 join 或者 sleep 可否满足我们的需要?答案是,不行!

  • 对于 join,这个方法必须要 t1 线程执行完毕 t2 才会运行,无法做到交叉配合运行。
  • 对于 sleep,这个方法虽然可以设定一个休眠时间,但是,两个线程之间配合工作,之间等待的时间是复杂且难以计算的,因此也不能使用。

需要注意的是,虽然 wait notify 相较于 join 等功能性更强,但是使用也相对会比较复杂。
注:wait,notify,notifyAll 都是 Object 类的方法。

1.wait 方法

wait 进行阻塞,当某个线程调用 wait 方法,这个线程就会处在阻塞状态 ( wait() 不加参数,就是一个“死等”,一直等待,直到有其他线程唤醒)

wait 的相关操作:

  • 使当前执行代码的线程进行等待。
  • 释放当前的锁。
  • 满足一定条件时被唤醒,重新尝试获取锁。

代码展示:

//直接使用 wait() 方法
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
            System.out.println("等待中");
            object.wait();
            System.out.println("等待结束");
    }

运行结果:
在这里插入图片描述
如图所示,程序报错,不难发现直接使用 wait() 方法是错误的。

我们需要注意到 wait 的内部操作中有这么一条 —— 先释放当前的锁,所以直接使用就会出现一个锁状态异常这样的情况。

因此 wait 操作需要搭配 synchronized 关键字来使用。先加锁,在解锁,再等待
代码展示:

    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object){
            System.out.println("等待中");
            object.wait();
            System.out.println("等待结束");
        }
    }

结果展示:
在这里插入图片描述
程序进入正常的运转。

2.notify() 方法

关于 notify() 方法就是唤醒正在等待的线程。

为了确保 notify() 可以正确通知,需要对正在等待的对象再次进行加锁

notify() 的相关操作:

  • 通知正在的进行等待的线程中的对象,使得该对象重新获取对象锁。
  • 如果多个线程进行等待,则由线程调度器随机调度一个 wait 状态的线程。
  • 在 notify() 方法后,当前线程不会马上释放对象锁,要等到执行 notify() 方法当前所在的代码块执行完毕才会释放对该对象的锁。

代码示例:

    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(()->{
            //这个线程负责等待
            System.out.println("t1 wait之前");
            synchronized (object){
                try {
                    //wait 会先释放锁,再将对象挂起等待
                    object.wait();
                    //wait 在被唤醒后会尝试在此获取当前的锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1 wait之后");
        });

        Thread t2 = new Thread(()->{
            System.out.println("t2 notify之前");
            // notify 获取到对应的元素的锁,才能进行通知
            synchronized (object){
                object.notify();
                //验证 notify 后不会直接释放该对象的锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("notify 当前代码块的其他代码。。。");
            }
            //从这开始就是抢占式执行了
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2 notify之后");
        });
        t1.start();
        //设定休眠时间确保 t1 等待线程先启动
        //防止 notify 空打一炮
        Thread.sleep(1000);
        t2.start();
    }

运行结果:
在这里插入图片描述

wait 和 notify 两个关键字是组合起来使用的,所以会比较复杂,所以下面我来简单总结一下:

设定对象 A

  1. wait 操作需要先解锁,因此,首先对对象 A 进行加锁
  2. 加锁后的对象会挂起等待,notify 想要通知 A 停止挂起,为了确保 notify作用在同一对象,需要让 notify 获取到 A,因此,需要对 A 再次进行加锁。
  3. 在 notify 关键字之后,并不会立即释放当前 A 对象的锁,当执行完 notify 关键字所在代码块的内容后,对所进行释放。
  4. 释放后,wait 关键字会重新尝试获取关键字 A 的锁,之后继续执行后续代码。

3. 关于 notifyAll() 方法

对于 notifyAll 方法的理解很简单,多个线程 wait 的时候,notify 随机唤醒一个,notifyAll 则是全部唤醒,让这些线程一块竞争锁。

4. wait 和 sleep 之间的简单比较

wait 关键字

  1. wait 需要搭配 synchronized 关键字使用。
  2. notify 唤醒 wait 不会出现任何异常。

sleep 关键字

  • interrupted 唤醒 sleep 会出现异常。(表示逻辑出现问题)

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

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

相关文章

硬件设计--stm32自动下载电路设计

1 参考博客&#xff1a; 1、Stm32 一键下载电路详解 2、启动模式&#xff0c;BOOT0和BOOT1详解 3、STM32自动ISP电路设计 4、STM32 USB接口 一键下载电路详解与过程分析 2 下载软件分享&#xff1a; 参考博客&#xff1a;FlyMcu - 用于STM32芯片ISP串口程序一键下载的免费软…

【速记】Postgresql中几个ResourceOwner的含义

几个ResourceOwner的含义 总结下几个resowner的含义&#xff1a; 事务结构内的resowner&#xff1a;TransactionState→curTransactionOwner 含义&#xff1a;跟随事务结构体创建&#xff0c;会申请内存&#xff0c;跟随事务结构释放。每层事务都有自己的curTransactionOwner…

Vivado 下按键实验

Vivado下按键实验 实验原理 PL通过按键的开关状态控制led的亮灭&#xff0c;按键按下的时候灯亮&#xff0c;按键未按下的时候灯灭。 这里的描述有些问题&#xff0c;PL_LED1为高的时候&#xff0c;LED两端的电压都为高&#xff0c;灯应该是不亮的&#xff0c;所以按照下面实…

口令暴力破解--Ftp协议暴力破解与Ssh协议暴力破解

Ftp协议暴力破解 FTP服务检测 FTP服务 FTP是一种文件传输协议&#xff0c; FTP服务默认端口为21。利用FTP服务器可以在本地主机和远程主机间进行文件传输。当FTP没有配置好安全控制&#xff0c;如对登录的源地址及密码尝试次数做限制&#xff0c;那么就会存在暴力破解可能。…

uniapp - 实现车牌号键盘与格子间隔显示组件,汽车牌照录入支持自定义样式、新能源等(附带组件完整源码,开箱即用,稍微改改就能用)

效果图 uniapp 全平台兼容,车牌号键盘输入、分格显示功能示例源码,注释很多! 可以直接复制一下,然后自己改改样式或功能就能使了。 示例源码 复制,运行。 &

基于神经网络的协同过滤-NCF

目录 1、摘要 2、引言 2.1协同过滤 2.2矩阵分解 3.准备工作 3.1学习隐性数据 3.2矩阵分解 ​3.3神经协同过滤 3.4广义矩阵分解 3.5多层感知器 3.6GMF和MLP的融合-NeuMF(神经矩阵分解) 4.总结 1、摘要 尽管最近的一些工作已经把深度学习运用到了推荐中&#xff0…

【mysql性能调优 • 一】mysql企业级安装部署(保姆级别教程)

前言 MySQL是一个关系型数据库管理系统&#xff0c;由瑞典MySQL AB 公司开发&#xff0c;属于 Oracle 旗下产品。MySQL是最流行的关系型数据库管理系统之一&#xff0c;在 WEB 应用方面&#xff0c;MySQL是最好的 RDBMS (Relational Database Management System&#xff0c;关系…

k8s 认证基础

0x00 前言 要想研究一个东西是不是存在安全问题&#xff0c;那么就要知道这个东西是什么&#xff0c;怎么用的&#xff0c;如何认证&#xff0c;认证方式等问题&#xff0c;了解这些才能更好的去解释学习安全问题。 0x01 访问控制 首先是k8s用户k8s用户分为service account以…

涨点技巧:Yolov5/Yolov7引入CVPR2023 Demystify Transformers Convolutions ,提升小目标检测精度

Demystify Transformers & Convolutions in Modern Image Deep Networks 论文:https://arxiv.org/pdf/2211.05781.pdf 视觉转换器最近的成功激发了一系列具有新颖特征转换范例的视觉主干,这些范例报告了稳定的性能增益。尽管新颖的特征转换设计通常被认为是收益的来源,…

0302Prim算法-最小生成树-图-数据结构和算法(Java)

文章目录1 Prim算法1.1 概述1.1.1 算法描述1.1.2 数据结构1.1.3 横切边集合维护1.2 延时实现1.2.1 实现代码1.2.2 性能分析1.3 即时实现1.3.1 分析1.3.2 实现代码1.3.3 性能分析结语1 Prim算法 1.1 概述 1.1.1 算法描述 算法描述&#xff1a; 初始化最小生成树&#xff0c;只…

Java classLoader

一. 什么是类加载器 类加载器 classLoader 类加载器&#xff1a;负责将.class文件(存储在硬盘上的物理文件)加载到内存中&#xff0c;是类加载器把类的字节码文件加载到内存当中的。二. 类加载的过程 每个编写的”.java”拓展名类文件都存储着需要执行的程序逻辑&#xff0c;…

vue项目Agora声网实现一对一视频聊天Demo示例(Agora声网实战及agora-rtc-vue使用,新增在线预览地址)

最终效果 在线预览地址 一、声网简介---->请查看官网 二、声网注册---->请自行百度&#xff08;创建音视频连接需要在Agora注册属于您的appid&#xff09; 三、具体实现视频聊天步骤 1、 实现音视频通话基本逻辑 1、创建对象 调用 createClient 方法创建 AgoraRTCCli…

ELK 日志系统收集K8s中日志

容器特性给日志采集带来的困难 • K8s弹性伸缩性&#xff1a;导致不能预先确定采集的目标 • 容器隔离性&#xff1a;容器的文件系统与宿主机是隔离&#xff0c;导致日志采集器读取日志文件受阻。 日志按体现方式分类 应用程序日志记录体现方式分为两类&#xff1a; • 标准…

最大似然估计法和Zero Forcing算法的思考

文章目录一、Zero Forcing 算法思想二、MMSE三、MIMO检测中 Zero Forcing 算法比 Maximum Likelihood 差的思考本篇文章是学习了B站UP主 乐吧的数学 之后的笔记总结&#xff0c;老师讲的非常好&#xff0c;大家有兴趣的可以关注一波&#xff01;一、Zero Forcing 算法思想 那…

Linux应用编程(文件IO基础)

1.1、个简单的文件 IO 示例 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main(void) {char buff[1024];int fd1, fd2;int ret;/* 打开源文件 src_file(只读方式) */fd1 open("./src_file",…

差分矩阵算法

前言&#xff1a;我们熟悉一维数组的前缀和和差分数组的相关操作和原理&#xff0c;但是对于二维数组也就是矩阵来说&#xff0c;它的差分和前缀和又会有什么不同之处呢&#xff1f;下面我们一起来研究&#xff0c; 1.二维数组的前缀和 首先&#xff0c;我们一般规定二维数组的…

【RocketMQ】主从同步实现原理

主从同步的实现逻辑主要在HAService中&#xff0c;在DefaultMessageStore的构造函数中&#xff0c;对HAService进行了实例化&#xff0c;并在start方法中&#xff0c;启动了HAService&#xff1a; public class DefaultMessageStore implements MessageStore {public DefaultM…

Vue2-黑马(六)

目录&#xff1a; &#xff08;1&#xff09;element-ui search搜索 &#xff08;2&#xff09;element ui Cascader级联选择器 &#xff08;3&#xff09;Router-路由配置 &#xff08;4&#xff09;Router-动态导入 &#xff08;1&#xff09;element-ui search搜索 我…

日撸 Java 三百行day21-22

文章目录说明day21 二叉树的深度遍历的递归实现1. 二叉树的遍历2. 二叉树深度&#xff0c;结点数3. 代码day 22 二叉树的存储1. 思路2.层次遍历代码3.代码说明 闵老师的文章链接&#xff1a; 日撸 Java 三百行&#xff08;总述&#xff09;_minfanphd的博客-CSDN博客 自己也把…

《程序员面试金典(第6版)》面试题 10.02. 变位词组

题目描述 编写一种方法&#xff0c;对字符串数组进行排序&#xff0c;将所有变位词组合在一起。变位词是指字母相同&#xff0c;但排列不同的字符串。 注意&#xff1a;本题相对原题稍作修改 示例: 输入: ["eat", "tea", "tan", "ate&quo…