[volatile]关键字和wait()notify()详解

news2024/11/19 21:20:57

目录

1.内存可见性问题-引入

2.volatile关键字

3.从java内存模型的角度内存可见性问题

4.wait()和notify()方法

4.1 wait()方法

4.2 notify()方法

wait与sleep的区别(面试题)

4.3 notifyAll()方法 

4.4 多个线程使用wait和notify方法


1.内存可见性问题-引入

构造一个myCounter类,成员flag,让t1线程中的循环条件为新创建的对象flag,让t2通过输入的整型值,控制flag的值,若非0,则t1循环应该终止!

class myCounter{
    public 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 sc = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            mycounter.flag = sc.nextInt();
        });
        t1.start();
        t2.start();
    }
}

我们先来看两个线程的状态:

 t1线程是RUNNABLE状态

因为还未输入,t2其实还是阻塞状态

当我们输入一个非0值后:

此时t2线程已经执行结束,t1线程还在RUNNABLE状态!!

可以看到并没有打印"t1循环执行结束".我们预期的是t2改动了flag值,t1就应该结束循环了,但此时t1明显没有结束循环

这个问题就是"内存可见性问题"!因为这里的结果并不是我们预期的,所以也是一个Bug,这也是一个线程安全问题!

这里使用汇编指令理解,大致分为两步操作:

1.load,把内存中的flag的值读取到寄存器中

2.cmp,把寄存器的值和0进行比较,根据比较结果,决定下一步的跳转(这两步是一个循环,执行速度极快)循环执行次数这么多,t2真正修改flag之前,load得到的结果都是一样的,另一方面,load的操作和cmp操作相比,速度则非常慢!!(CPU针对寄存器的操作,要比内存操作快很多,快3~4个数量级,计算机对于内存的操作,比硬盘快3~4个数量级)

由于load执行的速度相对于cmp比较慢,再加上反复load的结果相同,JVM就做出了一个优化,不再重复load,判定没有人修改flag的值,于是只读一次就好了

不再重复load是编译器的优化 的一种方式,但是实际上是有人修改的,因此由于编译器的判断失误导致出现了bug

内存可见性问题

一个线程针对一个变量进行读取操作,另一个线程针对这个变量进行修改,此时线程读到的值,不一定是修改过后的值!!读线程没有感受到线程的改动!!归根结底还是编译器/JVM在多线程环境下优化时产生了误判!

此时就需要程序员手动干预了

2.volatile关键字

如何手动干预编译器/JVM的优化呢?

此时,给flag变量加上volatile 关键字就可以了,这个单词意思是可变的,容易失去的.它表示这个变量时"可变的",编译器每一次都要重新读取这个变量的内存内容,任意时间这个变量的值可能改变,编译器不能对它进行优化!!!

给flag变量加上volatile 关键字 

结果

优化虽然能让速度提升起来,但是容易引发各种各样的问题!

这个关键字只能修饰变量,

不能修饰方法里的局部变量,局部变量出了方法就没了,只能在线程里边使用,不能多线程之间同时读取/修改,天然的规避了线程安全问题!

每个线程都有自己的"栈空间",方法内部的变量在"栈"这样的内存空间上(栈就是记录方法之间的调用关系),即使是同一个方法不同的线程调用,方法内的局部变量也在不同的栈空间中,本质上还是不同的变量,那么也不会涉及到多个线程读取/修改同一个变量的情况,不会出现内存可见性问题

 上面的内存可见性问题也不是始终会出现,就是可能会误判,如果加个sleep,我们看结果

 结果

这里结果正确了,sleep控制了循环的速度,编译器错误的优化也消失了,但是我们不知道编译器什么时候会优化,在应用程序方面无法感知,最稳妥的方法还是加上volatile

3.从java内存模型的角度内存可见性问题

java程序里,内存,每个线程还有自己的"CPU和寄存器"都是不同的,t1线程进行读取的时候,只是读取了t1线程的"CPU 寄存器"的值,t2线程进行修改的时候,先修改的是"CPU 寄存器"中的值,然后再把这个值同步到内存中,但是由于编译器优化,t1没有重新从内存中同步数据到"CPU 寄存器"中,读到的结果就是"修改之前的值"

主内存:main memory 贮存,也叫做内存

工作内存:work memory 工作存储区,不是内存,而是cpu上存储数据的单元(这里是存储器,还有其他东西(cache等))

这里的工作内存还不光指寄存器,还可能是高速缓冲器,cpu读取寄存器,速度比读取内存快太多了,为了减小这个差距,引入了cache,cache是指可以进行高速数据交换的存储器,它先于内存与CPU交换数据,因此速率很快

缓存的工作原理

当CPU要读取一个数据时,首先从CPU缓存中查找,找到就立即读取并送给CPU处理;没有找到,就从速率相对较慢的内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。正是这样的读取机制使CPU读取缓存的命中率非常高(大多数CPU可达90%左右),也就是说CPU下一次要读取的数据90%都在CPU缓存中,只有大约10%需要从内存读取。这大大节省了CPU直接读取内存的时间,也使CPU读取数据时基本无需等待。总的来说,CPU读取数据的顺序是先缓存后内存

相比于cpu和内存,它的存储空间居中,读写速度居中,成本居中,当CPU需要读到一个内存数据的时候,可能直接从内存读,也能从cache中的缓存,也可能读寄存器中的数据

因此硬件结构更复杂了,工作存储区=cpu寄存器+cpu cache,为了表述简单,直接就用"工作内存"代替了!

4.wait()和notify()方法

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知,但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序 

多线程引起的不安全问题往往是因为抢占式执行,随即调度,因此需要程序员来控制线程之间的执行顺序,虽然线程在内核里的调度是随机的,但是可以让线程阻塞,主动给放弃CPU,让别的线程先执行

例如,t1线程和t2线程,先让t1线程执行,t2线程先wait(阻塞),等待t1执行一部分,然后通过notify通知t2,唤醒t2,让t2执行(使用sleep和join也可以,但是wait和notify能更好地解决问题)

使用join,必须t1先执行完,t2才能执行.如果想让t1执行一般,t2就执行,join做不到!

使用sleep,是指定一个休眠时间,但是无法知道t1具体得花费多少时间不好估计,容易出现偏差

 wait,notify,notifyAll这几个方法都是Object类的方法,java中的所有类都是继承于这个类的,因此任意对象都有这三个方法!

4.1 wait()方法

看一个案例

public class ThreadDemo17 {
    public static void main(String[] args) throws InterruptedException {
        Object ob = new Object();
        ob.wait();
    }
}

 

 这里出发了不合法的监视器(synchronized)状态异常,锁的状态就分为加锁和解锁状态,非法就是预期的是什么状态,结果是另一个状态,产生异常就要直到wait干了什么事

wait的功能

释放当前的锁

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

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

当前ob对象没有加锁,因此wait还无法释放锁,就会产生非法的锁状态异常 

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

 

 我们看到wait之后线程就阻塞了,不会再执行,没有打印"wait之后"

注意:虽然这里wait之后主线程阻塞在synchronized代码块中,此时处于WAITING状态,但是这里的阻塞释放了锁,其它线程是可以获取到object这个对象的锁

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

 wait 结束等待的条件

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

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

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

4.2 notify()方法

notify 方法是唤醒等待的线程

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

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

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

notify也要搭配 synchronized 来使用

观察一个notify唤醒线程的案例

public class ThreadDemo18 {
    public static void main(String[] args) {
        Object object = new Object();
        //t1线程用来进行wait
        Thread t1 = new Thread(()->{
            synchronized (object){
                System.out.println("wait之前");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("wait之后");
            }
        });
        Thread t2 = new Thread(()->{
            System.out.println("notify之前");
            synchronized (object){
                //notify必须获取到锁才能进行通知
                object.notify();
            }
            System.out.println("notify之后");
        });
        t1.start();
         try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}

此处先执行了wait,然后wait操作阻塞了.没有看到紧接着wait之后的打印,接下来执行t2,进行了notify之后,才会唤醒t1的wait,t1才继续执行,打印wait之后.线程start之间添加sleep是保证大概率情况下,t1先于t2执行,否则t2先执行,notify时,没有对应的wait响应,那么t2就是进行了无效的通知,不过也没有别的影响,就是相当于白通知了!

如果t2不进行notify,那么t1就会一直阻塞,等待其它线程的唤醒,这样死等容易出问题

因此还提供了一个带参的wait(),参数为指定的等待的最大时间,等待最大时间还没有被唤醒,就直接自动唤醒,继续执行

wait与sleep的区别(面试题)

和wait比较相似,sleep也是休眠指定时间,也都能被提前唤醒,sleep是通过interrupt唤醒,wait是通过notify唤醒.但是表示的含义不同,notify是正常唤醒,逻辑是正常的,sleep被提前唤醒则是出现了异常,是不正常的逻辑

wait需要搭配synchronized使用,sleep不需要

wait是Object的方法,sleep是Thread的静态方法

4.3 notifyAll()方法 

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

看一个案例:设置多个线程,先使用notify方法,看唤醒了几个线程,再将notify替换为notifyAll,观察唤醒了几个线程

public class WaitAndNotify {
public static void main(String[] args) {
        Object co = new Object();
        System.out.println(co);

        for (int i = 0; i < 5; i++) {
            MyThread t = new MyThread("Thread" + i, co);
            t.start();
        }

        try {
            Thread.sleep(2000);
            System.out.println("-----Main Thread notify-----");
            synchronized (co) {
                co.notify();
            }

            Thread.sleep(2000);
            System.out.println("Main Thread is end.");

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class MyThread extends Thread {
    private String name;
        private Object co;

        public MyThread(String name, Object o) {
            this.name = name;
            this.co = o;
        }

@Override
        public void run() {
            System.out.println(name + " is waiting.");
            try {
                synchronized (co) {
                    co.wait();
                }
                System.out.println(name + " has been notified.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 

 5个线程都在阻塞中,使用notify方法,只唤醒了一个等待的线程

我们将notify换成notifyAll后:

 

 所有阻塞等待的线程都被唤醒

注意: 虽然是同时唤醒这些线程, 但是这些线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的执行.

4.4 多个线程使用wait和notify方法

案例:有三个线程,分别只能打印ABC,通过使用wait和notify方法控制三个线程按固定的顺序打印ABC

public class ThreadDemo19 {
    public static void main(String[] args) {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            System.out.println("A");
            synchronized (locker1){
                locker1.notify();
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (locker1){
                try {
                    locker1.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("B");
            synchronized (locker2){
                locker2.notify();
            }
        });
        Thread t3 = new Thread(()->{
            synchronized (locker2){
                try {
                    locker2.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("C");
        });
        t2.start();
        t3.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t1.start();
    }
}

程序中如果先执行t2的wait,后执行t1的notify, 是没问题的,但是可能存在这种情况:如果调度顺序是先t1中的notify,那么就不会唤醒t2了.程序就僵持在这里了.解决办法就是让t1执行慢点,让其他线程先执行,这样就万无一失了

结果

 

 

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

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

相关文章

阿里三面:说一说你在上家公司都用过哪些限流方案?

一、限流思路 常见的系统服务限流模式有&#xff1a;熔断、服务降级、延迟处理和特殊处理四种。 1、熔断 将熔断措施嵌入到系统设计中&#xff0c;当系统出现问题时&#xff0c;若短时间内无法修复&#xff0c;系统会自动开启熔断开关&#xff0c;拒绝流量访问&#xff0c;避…

数据库原理及MySQL应用 | 视图

视图是关系数据库系统提供给用户以多种角度观察数据库中数据的重要机制&#xff0c;透过视图用户可以看到数据表中看书需要的内容。 视图(View)是关系数据库系统提供给用户以多种角度观察数据库中数据的重要机制。在用户看来&#xff0c;视图是通过不同角度去看实际表中的数据&…

如何在GPU上运行pandas和sklearn?

当涉及大量数据时&#xff0c;Pandas 可以有效地处理数据。但是它使用CPU 进行计算操作。该过程可以通过并行处理加快&#xff0c;但处理大量数据仍然效率不高。 在以前过去&#xff0c;GPU 主要用于渲染视频和玩游戏。但是现在随着技术的进步大多数大型项目都依赖 GPU 支持&a…

【C++初阶7-string实现】xxx坐享其成,xxx苦不堪言

前言 本期分享Cstring类的模拟实现&#xff08;参考SGI 30实现&#xff09;&#xff0c;不套类模版&#xff0c;降低学习成本&#xff0c; 进一步加深理解。 属性介绍 名称具体成员动态字符数组char* _s存储有效数据的容量size_t capacity有效数据的个数size_t sizenposstat…

【OpenCV 例程 300篇】251. 特征匹配之暴力匹配

『youcans 的 OpenCV 例程300篇 - 总目录』 【youcans 的 OpenCV 例程 300篇】251. 特征匹配之暴力匹配 特征匹配是特征检测和特征描述的基本应用&#xff0c;在在图像拼接、目标识别、三维重建等领域的应用非常广泛。 基于特征描述符的特征点匹配是通过对两幅图像的特征点集合…

基于统一空间方法的动态切换拥挤(DSC)DSC-MOAGDE算法附matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

Python编程零基础如何逆袭成为爬虫实战高手之《WIFI破解》(甩万能钥匙十条街)爆赞爆赞~

导语 Hello&#xff0c;大家好呀&#xff01;我是木木子吖&#xff5e; 一个集美貌幽默风趣善良可爱并努力码代码的程序媛一枚。 听说关注我的人会一夜暴富发大财哦~ &#xff08;哇哇哇 这真的爱&#x1f60d;&#x1f60d;&#xff09; 所有文章完整的素材源码都在&#…

dreamweaver网页大作业 我的家乡——南京玄武湖旅游攻略(4页) 学生网页设计作业源码

家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法&#xff0c;如盒子的嵌套、浮动、margin、border、background等属性的使用&#xff0c;外部大盒子设定居中&#xff0c;内部左中右布局&#xff0c;下方横向浮动排列&#xff0c;大学学习的前端知识点和布局方式都有…

Java项目写好了,如何部署上线?看这篇文章吧

嗨&#xff0c;各位小伙伴大家好&#xff0c;你有没有想壹哥呀&#xff1f;前几天有小伙伴给壹哥留言&#xff0c;说自己的项目写好了&#xff0c;想把项目部署到服务器上&#xff0c;这个该怎么实现呢&#xff1f;那么针对这个问题&#xff0c;今天壹哥就带大家走一遍完整的项…

【云计算与大数据计算】大数据物理、集成、安全架构及阿里云飞天系统架构讲解(超详细)

一、物理架构 物理架构 - 企业大数据系统的各层次系统最终要部署到主机节点中,这些节点通过网络连接成 为一个整体,为企业的大数据应用提供物理支撑 &#xff0c;企业大数据系统由多个逻辑层组成&#xff0c;多个逻辑层可以映射到一个物理节点上&#xff0c;也可以映射到多个物…

nacos--基础--1.1--理论--介绍

nacos–基础–1.1–理论–介绍 1、介绍 是阿里的一个开源产品致力于帮助您发现、配置和管理微服务 1.1、参考资料 https://nacos.io/zh-cn/docs/what-is-nacos.html1.2、功能 服务发现中心&#xff1a;动态服务发现服务注册中心&#xff1a;管理注册服务服务配置中心&#…

行为管理(锐捷智慧教室)

大家好&#xff0c;我是小杜&#xff0c;被师傅“强制”休息两天&#xff0c;感觉整个人都升华了&#xff0c;精神满满的&#xff0c;看来还是需要劳逸结合&#xff0c;一味的高强度精神亢奋的情况下其实是事倍功半......。随着学习的深入&#xff0c;师傅也带着我对公司的业务…

PCIe Dma coherent

目录 1.PCIe Dma coherent前言 2.DMA与Cache 的一致性 2.1一致性问题 2.2Coherent DMA buffers 一致性 2.3DMA Streaming Mapping 流式DMA映射 2.4dma_alloc_coherent的例外 2.5SMMU | IOMMU 3.Linux 内核中 DMA 及 Cache 分析 3.1arm 3.2DMA ZONE 3.3DMA ZONE 的内…

南昌市-中安协-安防工程企业设计施工维护能力评价

安防工程企业设计、施工、维护能力评价是指中国安全防范产品行业协会从本行业实际出发&#xff0c;制定评价标准和实施办法&#xff0c;确定其能力等级&#xff0c;并颁发证书的活动。安防工程企业&#xff08;以下称企业&#xff09;设计、施工、维护能力&#xff08;以下称能…

以太网 传统STP生成树简介、STP工作方式简单介绍

2.10.0 以太网 传统STP生成树&#xff08;简介、工作方式&#xff09; 作用&#xff1a; STP&#xff08;Spanning Tree Prortoco&#xff09;生成树协议&#xff0c;它的出现解决了交换机网络环路的问题。 交换机网络中收到BUM帧的时候&#xff0c;将会进行泛洪的操作&…

CSS 父选择器,:has()

在CSS Selectors 4规范中&#xff0c;CSS 引入了一个名为 的新选择器:has()&#xff0c;它最终让我们可以选择父级。这意味着我们可以选择具有特定元素的父元素。目前Safari和Chrome105已经支持。 父选择器如何在 CSS 中工作 在 CSS 中&#xff0c;如果我们想要选择某些东西&a…

【Pygame小游戏】史上最全:《唐诗三百首》合集,每一首都是精华,果断收藏~(学诗+锻炼记忆+Python诗句填空小程序上线啦)

前言 岁岁年龄岁岁心&#xff0c;不负时光不负卿 哈喽&#xff01;我是你们的栗子同学&#xff0c;今天给大家来点儿有趣的—— 有句话说&#xff1a;“读史使人明智&#xff0c;读诗使人灵秀。”唐诗本来就是中国文化的绚丽瑰宝&#xff0c;是每个人都 该学习的人生必修课。…

蚂蚁三面遭分布式血虐,意外收获史诗级分布式笔记手册,从基础到进阶收获满满

学习分布式系统设计的难题在于&#xff0c;这个过程存在一个环境障碍&#xff0c;工作中只有一些中大规模的互联网企业&#xff0c;才有开发大规模分布式系统的场景和需求。这就造成目前只有少数身在一线互联网公司的架构师和开发者&#xff0c;才有机会接触并掌握分布式系统设…

Spring Security入门学习

认识Spring Security Spring Security 是为基于 Spring 的应用程序提供声明式安全保护的安全性框架。Spring Security 提供了完整的安全性解决方案&#xff0c;它能够在 Web 请求级别和方法调用级别处理身份认证和授权。因为基于 Spring 框架&#xff0c;所以 Spring Security…

树莓派Pico开发板与大功率MOSFET/IGBT器件驱动控制24V直流电机技术实践

摘要&#xff1a;本文在介绍MOSFET器件和IGBT器件作为电子开关基本原理的基础上&#xff0c;讲述了Pico与MOSFET&IGBT器件驱动控制24V直流电机硬件接口技术&#xff0c;最后给出了Pico开发板GP15端口控制24V直流电机启停的MicroPython测试程序。 一、实验设备和元器件清单 …