[JavaEE初阶] 内存可见性问题----volatile与wait(),notify()的使用

news2024/11/28 10:56:18

读书要趁黑发早,白首不悔少当时

文章目录

  • 1. 什么是内存可见性问题
  • 2. 避免内存可见性问题-----volatile(易变的)
  • 3. 需要注意的点
  • 4. wait()与notify()的使用
    • 4.1 控制两个线程执行顺序
    • 4.2 控制多个线程执行顺序
    • 4.3 wait()与sleep()的区别
  • 总结



1. 什么是内存可见性问题

线程A在读一个变量的时候,另一个线程B在修改这个变量,所以,线程A读到的值不是修改之后的,是一个未更新的值,读到的值是错误的.

如下代码,t1线程进行一个循环,循环条件是c.count = 0,线程2进行修改c.count值的操作.正常来说,t2线程修改了count的值,t1线程循环条件不满足,会跳出循环,打印,之后结束进程.但现实结果是,t1的循环一直没有结束,大家思考,这是为什么呢?

class Counter{
    public int count = 0;
}
public class Volatile {
    public static void main(String[] args) {
        Counter c = new Counter();
        Thread t1 = new Thread(()->{
            while(c.count == 0){

            }
            System.out.println("t1线程要结束啦~");
        });
        Thread t2 = new Thread(()->{
            Scanner in = new Scanner(System.in);
            int i = in.nextInt();
            c.count = i;
            System.out.println("t2线程要结束啦~");
        });
        t1.start();
        t2.start();
    }
}

执行结果如下,修改了count值后,线程t1一直没有结束
在这里插入图片描述

t1的循环条件,c.count = 0,这个比较操作需要两个具体操作才能完成.
1.每次将count的值读取到寄存器上,即load.
2.将寄存器中count的值与0进行比较,即cmp.

由于t1的循环执行速度非常快,1s能执行上百万次,并且比较值的操作cmp比读取值到寄存器的操作load要快得多,所以,这里编译器发现这里t1的循环读取的值貌似一直都是一个数,所以,这里编译器自作主张对程序做了个优化,只读一次count值,之后的循环都按第一次读到的值来进行比较.
正常时候,这个优化是没问题的,但这个是多线程程序,t2线程对count值进行了修改,t1没有察觉到,还是按第一次读取到的值0来进行比较,出现了线程安全问题----内存可见性问题,一个线程读,一个线程改,读到的数是修改之前的值,是错误的值.

2. 避免内存可见性问题-----volatile(易变的)

如下代码,用volatile修饰变量,这个操作是在告诉编译器,这个变量值有其他线程能修改,是能变化的值,防止编译器自作主张进行优化,避免只读取一次值的行为.t1线程每次循环都要重新读一次count值.

class Counter{
    volatile public int count = 0;
}

修改后,程序结果如下.
在这里插入图片描述

3. 需要注意的点

volatile不能修饰方法里的局部变量.由于不同线程调用方法时,都会开辟自己的栈空间,去单独使用变量,不同进程之间互不影响.(C++中volatile可以修饰局部变量,因为C++可以将线程A的局部变量给线程B使用)

4. wait()与notify()的使用

4.1 控制两个线程执行顺序

我们之前讲过,join()方法也能控制线程执行顺序,但join()方法是只能在一个线程执行完毕后才能执行另一个线程,控制的是进程结束的顺序.

线程A调用wait()方法,会释放锁,进入阻塞状态,让其他线程B先执行,直到线程B调用notify()方法,唤醒线程A.这里的notify()可以放在线程B的任意位置,可以使线程B执行一部分,就唤醒线程A,更为灵活.
wait()与notify()方法都属于object类中的方法.需要创建object对象来调用.

如下代码,线程t2调用wait()方法,t1与t2同样对对象o1加锁,线程t2就只能释放锁,进入阻塞状态,等到t1线程执行到notify(),通知t2,t2唤醒,进入执行状态.有效控制线程之间的执行顺序.

		Object o1 = new Object();
        Thread t1 = new Thread(() -> {
            System.out.print("A");
            synchronized (o1) {
                o1.notify();
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                synchronized (o1) {
                    o1.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("B");
        });
        t1.start();
        t2.start();

如下图,需要注意,只有notify()和wait()的对象是同一个的时候,才会起效果.
在这里插入图片描述
wait()方法也可以带参数,表示最长等待时间.
如下代码,wait()方法参数3000ms,只过了3s之后,若还没有别的线程调用notify()去唤醒线程t,t会自动唤醒.

			Thread t = new Thread(()->{
            try {
                synchronized (o1) {
                    o1.wait(3000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("最长等待时间后,t执行");
        });

4.2 控制多个线程执行顺序

同样的方法,控制三个线程的执行顺序,方法很简单,大家可以独立思考以下.
定义两个object对象,o1和o2,由o1控制线程A和线程B的执行顺序,对象o2控制线程B与线程C的执行顺序.

		Thread t1 = new Thread(() -> {
            System.out.print("A");
            synchronized (o1) {
                o1.notify();
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                synchronized (o1) {
                    o1.wait(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("B");
            synchronized (o2) {
                o2.notify();
            }
        });
        Thread t3 = new Thread(() -> {
            try {
                synchronized (o2) {
                    o2.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("C");
        });

这里需要注意一个问题,因为线程抢占式调度,t1的notify()方法有可能执行的比t2的wait()方法要早,这样,线程2就没有线程去唤醒它了,一直处在阻塞状态,出现了bug,t2与t3同理,所以,这里要控制,线程的开始顺序为t3,t2,t1,防止notify()比对应的wait()要早的情况.
如下代码,t3,t2之间加上sleep(),控制线程之间的开始顺序.

		t3.start();
        Thread.sleep(500);
        t2.start();
        Thread.sleep(500);
        //避免t2,t3的wait()比t1的notify要晚.t2,t3先执行,但都要释放锁.
        t1.start();

4.3 wait()与sleep()的区别

1.wait()方法需要搭配notify()使用.而sleep()可以单独使用
2.wait()是Object类中的方法,sleep()是Thread类中的静态方法.


总结

内存可见性问题出现在多线程中一线程读,一线程写造成的问题,由volatile修饰,防止编译器进行优化,每次重新读取值.
wait(),notify()可以控制线程之间的执行顺序.

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

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

相关文章

后端人眼中的Vue(四)

七、Vue生命周期 ​ Vue的生命周期指的是Vue实例在页面中创建到销毁整个过程。Vue提供了在各个生命周期的钩子,钩子也叫Vue生命周期函数。这些生命周期函数是伴随着Vue实例创建、销毁的过程中自动触发的(不需要人为手动触发)。Vue实例生命周期…

Leetcode:106. 从中序与后序遍历序列构造二叉树、105. 从前序与中序遍历序列构造二叉树(C++)

目录 106. 从中序与后序遍历序列构造二叉树: 问题描述: 实现代码与解析: 切割法(递归): 原理思路: 索引版本: 105. 从前序与中序遍历序列构造二叉树: 问题描述&am…

zookeeper单节点部署

kafkazookeeper单节点部署及注意事项 事前准备: 1、一台Linux服务器或者是一台虚拟机 2、准备好JDK环境 3、安装好wget(当然也可以不用这个,只是用于下载安装包的一个工具,所以能下载好包就是没问题的) 4、需要了解vim…

Java面试题每日10问(4)

Core Java - OOPs Concepts: Inheritance Interview Questions 1. Why use inheritance in java? For Method Overriding (so runtime polymorphism can be achieved).For Code Reusability. Terms used in Inheritance Class: –A class is a group of objects which have c…

皕杰报表点击导出按钮后网页变空白问题

有人反映使用皕杰报表导出时,点击导出按钮后网页变成了空白,然后就没有反应了。看tomcat控制台也没有错误信息,似乎遇到了一个很难缠的问题,没有错误信息却卡滞了,这个问题怎么解决呢? 还是要从tomcat的日志…

盘点微服务架构下的诸多身份验证方式

联合作者:罗泽轩,API7.ai 技术专家、Apache APISIX PMC 成员 联合作者:赵士瑞,API7.ai 技术工程师,Apache APISIX Committer 身份认证是授予用户访问系统并授予使用系统的必要权限的过程。而提供了这一功能的服务&…

指针详解——高级指针的解析及应用

目录 🐑指针的初步了解 🐂指针的深入认识 🦛1.指针数组 🐀指针数组的介绍 🐀指针数组的用法介绍 🐫2.数组指针 🦌数组指针的介绍以及使用 🦮3.函数指针 🐈函数指针的介绍…

Linux0基础入门:初识shell脚本编程

初识脚本编程到目前为止我们已经知道了 Linux 系统和命令行的基础知识,是时候开始编程了。本章讨论编写 shell 脚本的基础知识。在开始编写自己的 shell 脚本大作前,你必须了解这些基本概念。 使用多个命令到目前为止,你已经了解了如何使用 s…

Revit连接处理:阳台扶手和楼梯扶手,墙和梁

一、Revit中阳台扶手和楼梯扶手的连接处理 如图,有一些阳台扶手和楼梯扶手连接的地方,连接处需要进行处理。 1.在楼板合适的边缘处先画出楼梯 (1)单击“楼梯” (2)在楼梯类型属性对话框中修改楼梯属性 (3)绘制楼梯 为了定位方便、准确,首先要…

重塑底层逻辑,涅槃重生继续远航

背景介绍 从贫困县爬出来本硕均为211学校,在机械专业学习7年,有4年的时间热衷于编程学习。因此一路跨行到IT行业。 履历介绍 从毕业后一直在AI算法行业研究,呆过初创公司,目前在上市公司上班。尝尽IT的苦也吃过IT的甜。从毕业一…

【jQuery】常用API——jQuery效果

jQuery 给我们封装了很多动画效果,最为常见的如下:一、显示隐藏切换效果1. 显示语法规范 show([speed,[easing],[fn]]);显示参数:(1)参数都可以省略, 无动画直接显示。(2)speed&…

el-date-picker日期时间组件 报 placement 警告的解决方法

在使用el-date-picker组件时报这个警告,虽然不影响页面,但一打开页面跳出来一堆错误警告,实在受不了 解决办法:加上以下一行即可

无序字母对 -- 欧拉回路

洛谷:P1341 无序字母对题目描述前置知识欧拉路径定义判断是否为欧拉图思路code参考题目描述 题目描述 给定 n 个各不相同的无序字母对(区分大小写,无序即字母对中的两个字母可以位置颠倒)。请构造一个有 (n1) 个字母的字符串使得每…

同源、跨域的概念与实现

本文将结合周老师的讲义对同源与跨域这一前端经典问题进行系统的总结、整理。一起来坐牢,快! 1. 同源限制 1.1 历史背景 - 含义的转变 1995年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。 最初&…

爬虫代理Scrapy框架详细介绍4

Scrapy 框架 Scrapy实例 下载安装 pip install scrapy Hello World 创建工程 在 cmd 下切换到想创建 scrapy 项目的地方,然后使用命名 scrapy startproject tutorial 注:tutorial 为工程名 然后就会发现在当前位置会多出一个文件夹,名字是 tu…

C++——map|set介绍

目录 关联式容器 set set的构造 set的迭代器 set的容量 set修改操作 equal_range multiset map map的构造 map的迭代器 map的容量与元素访问 map测试 关联式容器 在初阶阶段,我们已经接触过STL中的部分容器,比如:vector、list、…

Linux内核权限提升漏洞

SSRF检测的一些思考 DNS平台没有立刻收到请求,是在之后的某个时间段收到了不同的请求信息,这至少表明了一点,此处存在有无回显的SSRF,虽然想要证明有更大的危害比较困难,但是至少说明了存在有SSRF的风险,所…

Maven 命令之将本地 Jar 包安装到 Maven 本地仓库

1、前言 Maven 是 Java 平台下的一款项目构建和依赖管理的自动化管理工具。 通过 Maven 远程仓库地址我们可以方便的管理 Jar 依赖包,但是在实际项目中有时候存在远程仓库中没有的 Jar 包,我们在项目中又必须要使用它,那就需要把本地 Jar 添…

HC-SR04超声波传感器使用

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录前言一、关于HC-SR04二、使用步骤1.确保驱动已经安装2.安装GPIO工具3.安装GPIO的Python支持4.Python3代码总结前言 最近在做一个项目,需要用到超声波传感…

誉辰智能拟科创板上市:欲募资4亿元,毛利率、研发费用率均下滑

近日,深圳市誉辰智能装备股份有限公司(下称“誉辰智能”)在上海证券交易所更新招股书(申报稿),披露时间为2023年1月7日,准备在科创板上市。据贝多财经了解,誉辰智能曾于2022年6月29日…