【JavaEE初阶】 volatile关键字 与 wait()方法和notify()方法详解

news2024/11/20 19:47:03

文章目录

  • 🌲volatile 关键字
    • 🚩volatile能保证内存可见性
    • 🚩volatile 不保证原子性
  • 🎋wait 和 notify
    • 🚩wait()方法
    • 🚩notify()方法
    • 🚩notifyAll()方法
    • 🚩理解notify 和 notifyAll
  • 🌳wait 和 sleep 的对比
  • ⭕总结

本文内容重点:

  • 了解并掌握volatile关键字的用法
  • 了解并掌握wait方法的使用
  • 明白wait与notify之间的联系
  • 知道wait 和 sleep 的区别

🌲volatile 关键字

首先我们知道,volatile是一个关键字,那么它有什么作用?又具有那些特性呢?请看下文

🚩volatile能保证内存可见性

volatile修饰的变量, 能够保证 “内存可见性”.

这是什么意思呢?

首先我们先来看一下下面这个例子:

  • 创建两个线程 t1 和 t2

  • t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.

  • t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.

  • 预期当用户输入非 0 的值的时候, t1 线程结束

class Conter{
    public int flag = 0;
}
public class TestMain {
    public static void main(String[] args) {
        Conter conter = new Conter();
        Thread thread1 = new Thread(() -> {
           while (conter.flag == 0) {
                //一系列操作
           }
        });
        thread1.start();
        Thread thread2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入:");
            conter.flag = scanner.nextInt();
        });
        thread2.start();
    }
}

但是当我们运行后我们会发现:
在这里插入图片描述
程序并没有跟我们想象中一样停止

这是什么原因呢?

其实这就与我们的内存有关了,这里博主暂时将内存分为两大类,主内存和工作内存

主内存一般是存储数据,工作内存做运算

而我们上述代码的 flag==0 的判断也需要经历两步

  • 第一步:从主内存上读取flag的值到工作内存中

  • 第二步:在工作内存中进行判断

由于第一步的执行时间相较于第二步太慢了

所以在编译时编译器会对该变量进行判断,若该变量后面不会进行修改,那么编译器就会自动进行优化,优化内容为

  • 只读取一次,以此提高运行速度

这就导致上述的flag一直保持这0,并没有读取新的flag

当然这属于编译器的误判,也不是100%会导致上述情况

比如我们对上述代码加上一个操作,我们在while循环里加上一个sleep的操作,降低了循环速度,这时候我们发现刚刚的误判消失了。

但是这种方法终究是奇技淫巧,而且也不是100%不会误判

所以我们面对这种情况Java引入里voltie关键字

volatile修饰的变量,不会被编译器优化,每一次都读取,这就保证了内存的可见性

在这里插入图片描述
代码在写入 volatile 修饰的变量的时候,

  • 改变线程工作内存中volatile变量副本的值

  • 将改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候,

  • 从主内存中读取volatile变量的最新值到线程的工作内存中

  • 从工作内存中读取volatile变量的副本

接下来我们用volatile关键字对代码进行修改,我们再看看效果,代码如下:

class Conter{
    public volatile int flag = 0;
}
public class TestMain {
    public static void main(String[] args) {
        Conter conter = new Conter();
        Thread thread1 = new Thread(() -> {
           while (conter.flag == 0) {
                //一系列操作
           }
        });
        thread1.start();
        Thread thread2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入:");
            conter.flag = scanner.nextInt();
        });
        thread2.start();
    }
}

结果为:
在这里插入图片描述
结果如我们所料

注意:我们使用vola关键字使得运算更加准确了,但是由于每次都会读取,就会降低我们的效率,所以具体情况具体使用,不要滥用

🚩volatile 不保证原子性

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

比如我们一下例子:
这个是最初的演示线程安全的代码.

  • 给 increase 方法去掉 synchronized

  • 给 count 加上 volatile 关键字.

 class Count {
 	//加入关键字
    public volatile int count = 0;
    //加锁
    // synchronized void increase() {
    //    count++;
    //}
    void increase() {
    	count++;
    }
}
public class Counter {
    public static void main(String[] args) throws InterruptedException {
        final Count count = new Count();
        //搞两个线程,分别对count进行++操作,每一个线程加50000次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count.count);
    }
}

我们会发现结果最终 count 的值仍然无法保证是 100000,依然存在线程不安全的问题

所以也就说明了volatile不保证原子性

🎋wait 和 notify

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知.

但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序

就比如:
在这里插入图片描述

球场上的每个运动员都是独立的 “执行流” , 可以认为是一个 “线程”. 而完成一个具体的进攻得分动作, 则需要多个运动员相互配合,按照一定的顺序执行一定的动作, 线 程1 先 “传球” , 线程2 才能 "扣篮

完成这个协调工作, 主要涉及到三个方法

  • wait() / wait(long timeout): 让当前线程进入等待状态.

  • notify() / notifyAll(): 唤醒在当前对象上等待的线程

注意: wait, notify, notifyAll 都是 Object 类的方法.

所以任何类都可以使用这三个方法

🚩wait()方法

wait 做的事情:

  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)

  • 释放当前的锁

  • 满足一定条件时被唤醒, 重新尝试获取这个锁

注意:wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.

wait 结束等待的条件:

  • 其他线程调用该对象的 notify 方法.

  • wait 等待时间超时 ( wait(long timeout) 方法提供一个带有 timeout 参数的版本, 来指定等待时间).

  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出InterruptedException 异常

代码示例: 观察wait()方法使用

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

这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify()

🚩notify()方法

notify 方法是唤醒等待的线程.

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。

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

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

代码示例: 使用notify()方法唤醒线程

  • 创建 thread1线程,里面调用一次wait

  • 创建 thread2线程,里面调用一次notify

  • 注意,thread1 和 thread2 内部持有同一个 Object locke(同一个锁)thread1和thread2 要想配合就需要搭配同一个 Object(同一个对象)

代码如下:

注意:notify方法要在wait方法后才会起作用,所以下面代码里,博主在notify前面加了sleep,确保threa1线程的wait先执行

public class WaitAndNotify {
    public static void main(String[] args) {
        Object locker = new Object();
        Thread thread1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("wait之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("wait之后");
            }
        });
        thread1.start();
        Thread thread2 = new Thread(() -> {
            synchronized (locker) {
            	 try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("notify之前");
                locker.notify();
                System.out.println("notify之后");
            }
        });
        thread2.start();
    }
}

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

🚩notifyAll()方法

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程

代码实例:

使用notifyAll()方法唤醒所有等待线程,

  • 创建 3 个 线程调用wait. 1 个 线程例调用notifyAll

代码如下:

注意:notifyAll方法要在wait方法后才会起作用,所以下面代码里,博主在notifyAll前面加了sleep,确保调用wait的线程先调用wait

public class WaitAndNotifyAll {
    public static void main(String[] args) {
        Object locker = new Object();
        Thread thread1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("thread1:wait之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("thread1:wait之后");
            }
        });
        thread1.start();
        Thread thread2 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("thread2:wait之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("thread2:wait之后");
            }
        });
        thread2.start();
        Thread thread3 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("thread3:wait之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("thread3:wait之后");
            }
        });
        thread3.start();
        Thread thread4 = new Thread(() -> {
            synchronized (locker) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("notify之前");
                locker.notifyAll();
                System.out.println("notify之后");
            }
        });
        thread4.start();
    }
}

我们来看一下调用结果:

在这里插入图片描述
此时可以看到, 调用 notifyAll 能同时唤醒 3 个wait 中的线程

注意: 虽然是同时唤醒 3 个线程, 但是这 3 个线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的执行,顺序随机,抢占式执行

🚩理解notify 和 notifyAll

notify 只唤醒等待队列中的一个线程. 其他线程还是乖乖等着
在这里插入图片描述
notifyAll 一下全都唤醒, 需要这些线程重新竞争锁
在这里插入图片描述

🌳wait 和 sleep 的对比

其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间

不同点在于:

  1. wait 需要搭配 synchronized 使用. sleep 不需要.

  2. wait 是 Object 的方法 sleep 是 Thread 的静态方法

⭕总结

关于《【JavaEE初阶】 volatile关键字 与 wait()方法和notify()方法详解》就讲解到这儿,感谢大家的支持,欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下!

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

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

相关文章

商贸进销存软件怎么选?

本文将为大家讲解&#xff1a;1、商贸企业进销存流程上的难点&#xff1b;2、商贸企业如何选择进销存系统&#xff1f; 在当今快速发展的商业环境中&#xff0c;有效的进销存&#xff08;Inventory Management&#xff09;管理是商贸企业成功的关键因素之一。然而&#xff0c;…

linux环境下使用lighthouse与selenium

一、安装谷歌浏览器、谷歌浏览器驱动、lighthouse shell脚本 apt update && apt -y upgrade apt install -y curl curl -fsSL https://deb.nodesource.com/setup_18.x | bash apt install -y nodejs apt install -y npm npm install -g lighthouse apt-get install -y …

AutoRunner自动化测试工具

AutoRunner自动化测试工具(简称AR&#xff09;是泽众软件自主研发的自动化测试工具&#xff0c;也是一个自动测试框架&#xff0c;加载不同的测试组件&#xff0c;能够实现面向不同应用的测试。通过录制和编写测试脚本&#xff0c;实现功能测试、回归测试的自动化&#xff0c;自…

UE4中无法保存项目问题

系列文章目录 文章目录 系列文章目录前言一、解决方法 前言 取消&#xff1a;停止保存所有资产并返回编辑器。 重试&#xff1a;尝试再次保存资产。 继续&#xff1a;仅跳过保存该资产。 当我点击继续时&#xff0c;关闭项目&#xff0c;然后重新打开项目&#xff0c;发现之前…

代码随想录

前言 代码随想录算法训练营day04 (不会写数据结构的代码&#xff0c;对着力扣题解和代码随想录网站边敲学) 一、Leetcode24. 两两交换链表中的节点 1.题目 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值…

手部关键点检测1:手部关键点(手部姿势估计)数据集(含下载链接)

手部关键点检测1&#xff1a;手部关键点(手部姿势估计)数据集(含下载链接) 目录 手部关键点检测1&#xff1a;手部关键点(手部姿势估计)数据集(含下载链接) 1. 前言 2. 手部检测数据集&#xff1a; &#xff08;1&#xff09;Hand-voc1 &#xff08;2&#xff09;Hand-vo…

FMI标准:实现SkyEye与Simulink无需缝合的联合仿真

在汽车工业、航空、机电装备等领域存在着不同的应用、建模系统。在解决不同问题的过程中&#xff0c;为了仿真完整的系统&#xff0c;往往需要在不同的仿真程序之间进行交互&#xff0c;并且为了实现完整的调试&#xff0c;系统的集成必须确保来自不同供应商的仿真环境能够协同…

腾讯云 CODING 界面全新升级,代码仓库 Rebase 变基合并、批量复制事项等功能上线!

点击链接了解详情 金秋十月&#xff0c;腾讯云 CODING DevOps 焕新上线&#xff01;本次更新&#xff0c;我们不仅推出了全新的用户界面&#xff0c;还推出了一系列重磅产品能力&#xff0c;满足广大用户的日常研发与协作所需。 以下是 CODING 新功能速递&#xff0c;快来看看是…

文件的操作

前言&#xff1a;哈喽小伙伴们好久不见&#xff0c;国庆假期已经结束&#xff0c;接下来我们还是要马不停蹄的投入到学习当中&#xff0c;只有尽快调整状态回归学习&#xff0c;才能弯道超车。 今天我们一起来学习C语言——文件操作。 本篇文章讲到的所有函数均需要头文件#inc…

国内ITSM发展的趋势

多年来&#xff0c;随着客户业务需求、工作文化、技术创新的不断变化以及新的IT环境的出现&#xff0c; IT支持也出现了新的变化&#xff0c;由单一的IT帮助台&#xff08; IT help desk&#xff09;逐渐转变为了综合性的IT服务台&#xff08; IT service desk&#xff09;&…

基于intel D4系列深度相机 (D455)的单点测距和两点距离测量(代码讲解超详细)

目录 1.简介 1.1 intel D4系列深度相机 (D455) 1.2结构光(Structured-light) 2.基于深度相机的单点测距和两点距离测量 2.1函数详解 2.2 全部代码 1.简介 1.1 intel D4系列深度相机 (D455) Intel RealSense D435、D455等D4系列&#xff1a; Intel D4系列深度相机是由英特…

有人因它丢掉offer!背景调查到底有什么魔力?

求职时&#xff0c;公司往往会对应聘者过往经历做一些背景调查——也被叫做背调。 据i背调人才研究院数据显示&#xff0c;2022年的求职失信占比仍然较高&#xff0c;有39.13%的候选人存在职业信用异常。而随着近些年来&#xff0c;整体就业市场和职业发展中竞争压力不断加剧&…

间歇性微服务问题...

在Kubernetes环境中&#xff0c;最近由于特定配置导致Pod调度失败。哪种 Kubernetes 资源类型&#xff08;通常与节点约束相关&#xff09;可能导致此故障&#xff0c;尤其是在未正确定义的情况下&#xff1f; 节点选择器资源配额优先级污点Pod 中断预算 已有 201 人回答了该…

【学习之路】Multi Agent Reinforcement Learning框架与代码

【学习之路】Multi Agent Reiforcement Learning框架与代码 Introduction 国庆期间&#xff0c;有个客户找我写个代码&#xff0c;是强化学习相关的&#xff0c;但我没学过&#xff0c;心里那是一个慌&#xff0c;不过好在经过详细的调研以及自身的实力&#xff0c;最后还是解…

A星寻路优化方案

1.存储常用位置间的路径信息&#xff08;此优化方案一般用于地图较大&#xff0c;且起始点离终点较远情况下&#xff09; 对于地图信息已知且各障碍位置固定的情况下&#xff0c;可以将常用的位置及其之间的路径信息保存&#xff0c;然后寻路时分别找到离开始点&#xff0c;结…

【JUC】原子操作类及LongAddr源码分析

文章目录 1. 十八罗汉2. 原子类再分类2.1 基本类型原子类2.2 数组类型原子类2.3 引用类型原子类2.4 对象的属性修改原子类2.5 原子操作增强类 3. 代码演示及性能比较&#xff1a;4. LongAddr原理5. LongAddr源码分析5.1 add()5.2 longAccumulate()5.3 sum() 6. 小总结6.1 Atomi…

【JUC】CAS原理

CAS原理 文章目录 CAS原理1. 概述2. 原理3. Unsafe4. CAS与自旋锁 1. 概述 compare and swap的缩写&#xff0c;中文翻译成比较并交换,实现并发算法时常用到的一种技术。它包含三个操作数一一内存位置、预期原值及更新值 执行CAS操作的时候&#xff0c;将内存位置的值与预期原…

和鲸 × 北中医:高规格、高并发,一场真正的人工智能分析应用临场实践考核

百舸争流&#xff0c;奋楫者先&#xff0c;2023 年&#xff0c;人工智能已逐渐成为引发千行百业产生颠覆性变革的前沿技术。随着人工智能时代的加速到来&#xff0c;我国政产学研各界对于创新型、复合型 AI 人才的能力要求正不断提升。 面对一批又一批激流勇进、乘风破浪的莘莘…

【Leetcode】 738. 单调递增的数字

当且仅当每个相邻位数上的数字 x 和 y 满足 x < y 时&#xff0c;我们称这个整数是单调递增的。 给定一个整数 n &#xff0c;返回 小于或等于 n 的最大数字&#xff0c;且数字呈 单调递增 。 示例 1: 输入: n 10 输出: 9 示例 2: 输入: n 1234 输出: 1234 示例3 : …

精通Maven的捷径:一文包揽所有必知必学

Maven是每个Java程序都会遇到的包管理工具&#xff0c;今天整理一下Maven的相关知识&#xff0c;从青铜到王者&#xff0c;一文全了解&#xff0c;我们开始吧&#xff01; 1、maven是什么&#xff0c;为什么存在&#xff1f;项目结构是什么样子&#xff0c;怎么定位jar 官方网…