线程学习(3)-volatile关键字,wait/notify的使用

​ 💕"命由我作,福自己求"💕
作者:Mylvzi
文章主要内容:线程学习(2)​​在这里插入图片描述​​

一.volatile关键字

volatile关键字是多线程编程中一个非常重要的概念,它主要有两个功能:保证内存可见性,和禁止指令重排序

1.内存可见性

内存可见性(Memory Visibility) 指的是在多线程编程环境下,一个线程修改共享变量,其他线程能够立即看到共享变量修改后的值

先来了解一个底层知识,我们写的代码是要读取数据的,最常见的数据就是我们定义的变量,变量被保存在内存之中,系统要使用数据需要cpu进行读内存(load)的操作,而load这个操作对于cpu来说是一个非常慢的数据,因为cpu的速度是很快的,读内存这个操作比读寄存器要慢上几千倍,所以load对于cpu来说是一个很大的开销

// 设计一个标志位
private static int isQuit = 0;

Thread t1 = new Thread(() -> {
	while(isQuit == 0) {
	// ......	
	}
	System.out.println("t1线程结束");
});

// 在t2线程中修改isQuit的值
Thread t2 = new Thread(() -> {
	System.out.println("请输入isQuit的值:");
	Scanner scan = new Scanner(System.in);
	isQuit = scan.nextInt();
});

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

结果截图:
在这里插入图片描述
在t2中将isQUit设置为1,按理说t1中的循环条件已经不满足了啊,整个进程应该会终止才对,但是什么都没有打印,这是为什么呢?这其实就和我们上面说的读内存对cpu开销大这一事实有关

先说结论,为了解决读内存的问题,并提高效率,编译器会对读取数据进行一些优化,减少读内存的次数,尽可能多的直接从cpu上读取,来提高效率

知道了这个结论,就很容易解释上述问题了:
在这里插入图片描述
编译器是好心的,但是他为了提高效率却忽视了准确性,而这个准确性对我们来说是很重要的(这涉及到整个进程的执行),不能忽视。这也属于一个编译器的bug

而关键字volatile就是用于解决这个问题,被volatile修饰的变量,编译器不会对其执行上述的优化,也就是告诉编译器,我这个变量很重要,不要为了效率就忽视准确性

当我们在t2线程中修改变量isQuit时,实际上是内存中的isQuit发生了改变,其他线程(t1)能够立即看到这个改变后的值,循环终止,这就是volatile关键字的内存可见性功能,它保证了共享变量在所有线程中的公开,透明,其他线程能够立即看到共享变量的改变,及时做出调整。
在这里插入图片描述
在输入1之后,t1线程结束循环,立即终止
内存可见性也是线程安全问题的一种,之前学习过的一个线程安全问题是两个线程同时针对同一个变量进行修改,但实际上两个线程,一个线程修改变量,一个线程读取变量可能也会发生线程安全问题

再补充一点,编译器之所以进行优化是因为短时间内大量的load操作.如果我们放慢/减少 load,编译器就不会进行优化,最常见的方式就是添加sleep

        Thread t1 = new Thread(() -> {
            while(isQuit == 0) {
                // 让线程休眠一会儿
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // ......
            }
            System.out.println("t1线程结束");
        });

运行结果:
在这里插入图片描述
由于t1内部让线程短暂阻塞了一会儿,load操作执行的次数就会大大减少,此时编译器就没有进行优化的必要,所以每次读取都是读内存.

补充:
volatile的本意是不稳定的,易挥发的,使用它修饰一个变量相当于告诉编译器"喂(#`O′),我这个变量是不稳定的啊,你不要把他给我加载到工作内存(cpu寄存器)中,让他老老实实的代码主内存中就行",这样编译器就不会进行优化了

2.指令重排序

指令重排序也是编译器优化的一种,它是指在保证执行结果正确的前提下,对指令的执行顺序进行调整,达到效率提高的目的,比如去菜市场买菜,我只要最后买到我要买的菜即可,我按照什么路径去买无所谓,但是我追求最快买完
在这里插入图片描述
在单线程模式下,指令重排序不会带来问题,反而还是一个好处.但是在多线程下,由于线程的调度是随机的,就有可能带来意想不到的 结果,所以我们需要禁止指令的重排序,往往是对可能涉及到指令重排序对象进行volatile的修饰,就能让编译器不对其进行优化

关于指令重排序 的具体应用会在后面的单例模式部分讲到

二.wait/notify

1.引言

在多线程编程中,我们经常要协调线程的执行顺序来实现一些场景需求,比如之前学习过的join()方法,他可以控制线程的结束顺序,也是协调线程的一种方式

但是有些时候我们想线程不结束,也能控制他们的执行顺序.而不是只能等到一个线程结束再去执行其他线程,此时,就可以使用wait/notify来实现上述需求

2.wait方法的使用

wait和notify方法都是属于Object类的方法,需要通过实例化一个object对象来进行调用
wait方法的具体执行过程分为三步:

  1. 释放当前对象的锁
  2. 阻塞等待
  3. 等待对象使用notify方法唤醒

wait方法使用的过程中有一个最常见的错误就是调用wait方法的对象事先并没有加锁,直接wait会报错

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        System.out.println("wait 开始");
        locker.wait();
        System.out.println("wait 结束");
    }

运行结果:
在这里插入图片描述
这里显示**"非法监视器状态异常"**,监视器 是什么?我们之前学习过synchronized,他是用于给对象加锁的,他其实还有另一个名字,叫做 监视器锁.

更本质的说,监视器(monitor)其实是一种机制,每个对象都会关联一个监视器,用于实现同步,同步就是保证多个线程对于共享变量的使用是合理的,是没有线程安全问题的(比如使用加锁来实现同步,可以避免多个线程针对同一变量进行修改这种线程安全问题).我想,这里的同步的意思是指在多线程编程中,每个线程获取到的共享资源的状态是同步的,不会出现一个线程修改了共享变量,另一个线程却还在使用修改之前的变量.

synchronized被称为监视器锁,是因为它本质上是获取到了对象关联监视器的锁,他保证了同一时间只能有一个线程访问进入synchronized修饰的代码块/方法,保证了线程安全.

回到本文,由于创建的locker对象事先并没有加锁,与其关联的监视器的并没有上锁,没有上锁你却想先释放锁,这不是bug吗,所以会报出非法监视器状态异常,为了解决这个异常,我们需要先对locker对象进行加锁

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        synchronized(locker) {
            System.out.println("wait 开始");
        	locker.wait();
        	System.out.println("wait 结束");
		}
    }

运行结果
在这里插入图片描述
注:wait方法也可以带参数,和带参数的join方法一样,可以设置等待的时间

3.notify方法的使用

wait方法会先释放调用对象的锁,然后使调用的线程处于阻塞状态,直到对象使用notify进行唤醒,notify在使用的时候也需要先获取到与对象关联的锁,否则也会抛出非法监视器状态异常,这样做也是为了保证线程安全
示例代码:

    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(() ->{
            synchronized (locker) {
                System.out.println("wait 开始");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                System.out.println("wait 结束");
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            
            synchronized (locker) {
                System.out.println("使用notify 方法 进行唤醒");
                locker.notify();
            }
        });

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

运行结果:
在这里插入图片描述
除了使用notify,我们还可以使用notifyAll方法来一次性唤醒当前对象所有wait的线程,当然看似是一次性,实际上还是一个一个进行唤醒的,如果一次性唤醒全部,会导致锁冲突,出现线程不安全问题

今天的学习就到这里,下期预告<<多线程设计模式讲解(1)>>

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JVM GC 算法原理概述

对于JVM的垃圾收集&#xff08;GC&#xff09;&#xff0c;这是一个作为Java开发者必须了解的内容&#xff0c;那么&#xff0c;我们需要去了解哪些内容呢&#xff0c;其实&#xff0c;GC主要是解决下面的三个问题&#xff1a; 哪些内存需要回收&#xff1f; 什么时候回收&…

Pandas教程(二)—— 不同格式的数据读取

前言&#xff1a;几种常用数据格式的介绍 csv文件 1. 逗号分隔值文件&#xff0c;以纯文本形式&#xff08;记事本&#xff09;存储表格数据 2. 它是一种平面文件&#xff1a;即只存储数据和文字&#xff0c;不能存储公式、图表等 3. 更适合存储大数据&#xff0c;一般用来批…

GitLab 删除或移动项目

首先明说&#xff0c;删除后无法恢复 第一步&#xff1a;找到要删除的项目 第二步&#xff1a;进入目录后&#xff0c;左侧菜单&#xff0c;设置 >>> 通用&#xff0c;拉到最下面找到“高级”&#xff0c;点击右侧“展开” 第三步&#xff1a;点击“展开”后往下拉&a…

作业--day37

课上strcut的练习改成class&#xff0c;并写一个有默认参数的函数&#xff0c;把声明和定义分开&#xff0c;并在主函数内成功调用 #include <iostream> #include <iomanip> #include <cstring>using namespace std;class stu{ private:int age;char sex;fl…

理解文件系统

一 什么是文件系统 文件系统是计算机操作系统中的一个核心组件&#xff0c;用于管理计算机中的文件和文件夹。它提供了一种组织和访问计算机存储设备上数据的方式。文件系统使用户能够创建、修改、删除和查找文件&#xff0c;以及组织文件和文件夹的层次结构。 ps: linux一共有…

MongoDB文档操作

3.3 文档操作 3.1 文档介绍 文档的数据结构和 JSON 基本一样。 所有存储在集合中的数据都是 BSON 格式。 BSON 是一种类似 JSON 的二进制形式的存储格式&#xff0c;是 Binary JSON 的简称。 文档是一组键值(key-value)对(即 BSON)&#xff0c;一个简单的文档例子如下&…

vue+element实现动态表格:根据后台返回的属性名和字段动态生成可变表格

现有一个胡萝卜厂生产不同品种的胡萝卜&#xff0c;为了便于客户了解产品&#xff0c;现需在官网展示胡萝卜信息。现有的萝卜信息&#xff1a;编号&#xff08;id&#xff09;、名称&#xff08;name&#xff09;、保质期&#xff08;age&#xff09;、特点&#xff08;remark&…

Java架构师系统架构设计实践

目录 1 导语2 架构设计实践本章概述3 架构设计要素概述和规划4 架构设计模式5 架构设计输入6 架构设计输出7 架构设计要素总结 想学习架构师构建流程请跳转&#xff1a;Java架构师系统架构设计 1 导语 Java架构师在进行系统架构设计时&#xff0c;需要综合考虑多个方面&#…

c语言:计算1+2+3……+n的和|练习题

一、题目 输入一个数n&#xff0c;计算123……n的和 二、代码截图【带注释】 三、源代码【带注释】 #include int main() { int num0; printf("请输入要运算的数:"); scanf("%d",&num); sumResult(num);//相加结果函数 } //计算打印…

使用CRA(create-react-app)初始化一个完整的项目环境(该初始化项目已上传到本文章的资源)

1. 整理项目结构&#xff0c;项目目录结构大致划分如下&#xff1a; 2. 安装sass 安装sass开发环境, 注意&#xff1a;使用的文件后缀名要用.scssnpm i sass -D3. 安装Ant Design npm i antd --save 4. 配置基础路由Router&#xff08;具体可参考ReactRouter使用详解&#x…

具有置信度学习的困难感知注意力网络用于医学图像分割

Difficulty-Aware Attention Network with Confidence Learning for Medical Image Segmentation 具有置信度学习的困难感知注意力网络用于医学图像分割背景贡献难点&#xff1a;实验方法分割网络Segmentation Network&#xff08;分割网络&#xff09;Fully Convolutional Con…

2024财年Q2财报:大中华区增长放缓,耐克压不住国货势头?

“去库存”一直是耐克的关键词。 今年&#xff0c;有关耐克打折促销活动的公众号推文层出不穷&#xff0c;举办该活动的奥特莱斯门店也因此成为了年轻一代新的打卡圣地。以广州万国奥特莱斯于今年9月新开设的全球最大“NIKE优选体验店”为例&#xff0c;该店开业当天人气爆棚&…

ElasticSearch 搜索数据

精确查询 存在查询 Exists query 用于查询某个字段不为空的数据。如下所示&#xff0c;查询 age 不为空的 数据 POST user/_search {"query": {"exists": {"field": "age"}} }主键查询 通过 _id 字段查询数据 POST user/_search …

宏集方案 | 物联网HMI的关键驱动力—SCADA级功能库和控件库

来源&#xff1a;宏集科技 工业物联网 宏集方案 | 物联网HMI的关键驱动力—SCADA级功能库和控件库 原文链接&#xff1a;https://mp.weixin.qq.com/s/UEPtpTehdbFrw3MUCnuR2A 欢迎关注虹科&#xff0c;为您提供最新资讯&#xff01; 01 前言 在这个数字化时代&#xff0c;物…

Dubbo入门直接上手,结合微服务详解

Dubbo 高性能、轻量级的 Java RPC 框架 RPC&#xff1a; Remote Procedure Call 远程过程调用&#xff0c;简单来说就是它允许一个计算机程序通过网络请求调用另一个计算机上的程序&#xff0c;就像本地调用一样。有非常多的协议和技术来都实现了RPC的过程&#xff0c;比如&a…

第二课:布尔逻辑与逻辑门、二进制及算术逻辑单元

第二课&#xff1a;布尔逻辑与逻辑门、二进制及算术逻辑单元 第三章&#xff1a;布尔逻辑与逻辑门1、计算机为什么使用二进制2、布尔代数&布尔代数在计算机中的实现1&#xff09;NOT 操作2&#xff09;AND 操作3&#xff09;OR 操作 3、特殊的逻辑运算——异或4、逻辑门的符…

【Python】ubuntu python>3.9编译安装,及多个Python版本并存的使用方法

【Python】ubuntu python3.9编译安装&#xff0c;及多个Python版本并存的使用方法 1. 安装依赖2. 编译与安装2.1 依赖与源获取2.2 配置2.3 编译2.4 安装2.5 链接动态库 1. 安装依赖 更新系统软件 在正式开始之前&#xff0c;建议首先检查系统软件是否均为最新&#xff0c;并更…

处理urllib.request.urlopen报错UnicodeEncodeError:‘ascii‘

参考&#xff1a;[Python3填坑之旅]一urllib模块网页爬虫访问中文网址出错 目录 一、报错内容 二、报错截图 三、解决方法 四、实例代码 五、运行截图 六、其他UnicodeEncodeError: ascii codec 问题 一、报错内容 UnicodeEncodeError: ascii codec cant encode charac…

IDEA2023创建web项目

一、新建项目 点击File->New->Project...&#xff0c;如果是第一次创建项目则单击New Project 二、添加Web Application 建好的样子 把web移动到main目录下同时改名为webapp 三、不存在Add Framework Support添加Web Application 如何存在Add Framework Support&#…

Java - 工厂设计模式

Java - 工厂设计模式 一. 简介二. 例子2.1 定义抽象类2.2 定义子类2.3 创建工厂2.4 测试 三. JDK中使用工厂模式的案例 前言 这是我在这个网站整理的笔记,有错误的地方请指出&#xff0c;关注我&#xff0c;接下来还会持续更新。 作者&#xff1a;神的孩子都在歌唱 工厂设计模式…
最新文章