【JavaEE初阶系列】——带你了解volatile关键字以及wait()和notify()两方法背后的原理

news2024/12/22 22:15:37

目录

🚩volatile关键字

🎈volatile 不保证原子性

🎈synchronized 也能保证内存可见性

🎈Volatile与Synchronized比较

🚩wait和notify

🎈wait()方法

💻wait(参数)方法

🎈notify()方法

🎈解决线程饿死方式

🎈notifyAll()方法


🚩volatile关键字

volatile修饰的变量 能保证内存可见性
在学习Java多线程编程Q里, volatile 关键字 保证内存可见性的要点时,看到网上有些资料是这么说的: 线程修改一个变量,会把这个变量先从主内存读取到工作内存;然后修改工作内存中的值,最后再写回到主内存。
 
内存可见性问题 的表述为: t1 频繁读取主内存(内存),效率比较低,就被优化成直接读自己的工作内存(cpu寄存器);t2 修改了主内存的结果,但由于 t1 没有读主内存,导致修改不能被识别到,最终导致代码出现bug。
计算机运行的程序/代码,经常要访问数据。
这些依赖的数据 往往会存储在内存中去~(定义一个变量,变量就是在内存中)
  • cpu使用这个变量的时候,就会把这个内存中的数据,先读出来,放到cpu的寄存器中。再参与运算(load) 
  • cpu读取内存的这个操作,其实非常慢!(快,慢 都是相对的)
  • cpu进行大部分操作,都很快,一旦操作到读/写内存,此时的速度就降下来了。
  • 读内存 >> 读硬盘 快几千倍,上万倍
  • 读寄存器 >> 读内存  快几千倍,上万倍
结论:为了解决上述的问题,提高效率,此时编译器,就可能对代码做出优化,把一些本来要读内存的操作,优化成读取寄存器。减少读内存的次数,也就可以提高整体程序的效率了。

此时我们进行下面的代码段,首先我们默认isQuit是0,t2线程输入isQuit的值,t1线程中如果isQuit一直都是0的话,一直死循环,如果isQuit !=0的时候,我们才判断t1线程结束。

public class test1 {
    private static int isQuit=0;
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            while (isQuit==0){
            }
            System.out.println("t1线程退出");
        });

        Thread t2=new Thread(()->{
            System.out.println("请输入isQuit: ");
            Scanner scanner=new Scanner(System.in);
            //一旦用户输入的值,不为0,此时就会使t1线程执行结束
            isQuit=scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

此时我输入isQuit是1,然后t1线程理想的结果是跳出循环,然后输出t1线程退出。

但是,当我真正输入1的时候,此时t1线程并没有结束,t1线程正在执行,并且是RUNNABLE状态。很明显,实际效果和预期效果是不一样的,由于多线程引起的,也是线程安全的问题。之前是俩个线程同时修改同一个变量,现在是一个线程读,一个线程修改,也可能出现问题。此处 的问题,就是"内存可见性"情况引起的。

  • 1> load读取内存中的isQuit的值到寄存器中
  • 2>通过cmp指令比较寄存器的值是否是0,决定是否要继续循环

因为读寄存器的速度>>读内存的速度,所以短时间内,就会进行大量的循环,也就是进行大量的load和cmp操作。此时,编译器jvm就发现了,虽然进行了这么多次load但是load出现的结果都是一样的,并且load操作又非常的消耗时间,一次load花的时间相当于上万次的cmp了。所以编译器就做出了优化,只是第一次循环的时候,才读内存,后面都不再读内存了,而是直接从寄存器中,取出isQuit即可。

原本是load读取到内存中到寄存器中,然后cmp指令在寄存器中比较,依次来,但是由于cmp指令速度太快了大于load操作。编译器的初心是好的,它是希望提高程序的效率,但是提高效率的前提是保证逻辑不变。此时由于修改isQuit代码是另一个线程的操作,编译器没有正确的判定,所以编译器以为没人修改isQuit,就做出了上述优化,也就进一步引起了bug了。

后续 t2线程修改isQuit之后,t1感知不到isQuit变量的变化(感知不到内存的变化),所以一直比较,一直死循环。

解决上述 这个问题,volatile就是解决方案,在多线程的环境下,编译器对于是否要进行这样的优化,判定不一定准。就需要程序员通过volatile关键字告诉编译器,你不要优化(优化是算的快,但是算不准)。

这也告诉我们编译器也不是万能的,也会有一些短板的地方,此时就需要程序员进行补充了。只需要给isQuit加volatile关键字修饰,此时编译器自然就会禁止上述优化过程。

此时,程序就可以顺利退出了。


但是还有一种方式,就是让cmp指令比较的速度变慢,让处于休眠状态,这时候,load操作的开销就不大了,优化就没必要了。

public class test1 {
    private static int isQuit=0;
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            while (isQuit==0){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t1线程退出");
        });

        Thread t2=new Thread(()->{
            System.out.println("请输入isQuit: ");
            Scanner scanner=new Scanner(System.in);
            //一旦用户输入的值,不为0,此时就会使t1线程执行结束
            isQuit=scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

但是我们编译器什么时候对其进行优化这是说不清楚的事情,所以用volatile修饰是最靠谱的事情。


🎈volatile 不保证原子性

volatile synchronized 有着本质的区别 . synchronized 能够保证原子性 , volatile 保证的是内存可见性. 代码示例
这个是最初的演示线程安全的代码 .
  • increase 方法去掉 synchronized
  • count 加上 volatile 关键字.
class Counter{
 volatile public int count = 0;
    void increase() {
     count++;
    }
}
   public  class Test2 {
       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);
       }
   }

此时的结果依旧不是1w。所以可以证明volatile是不能保证原子性的。


🎈synchronized 也能保证内存可见性

synchronized 既能保证原子性 , 也能保证内存可见性 . 对上面的代码进行调整:
  • 去掉 isQuit  volatile
  • t1 的循环内部加上 synchronized, 并借助object对象加锁.
public class Test3 {
        private static  int isQuit=0;
        public static void main(String[] args) {
            Object object=new Object();
            Thread t1=new Thread(()->{
                while (true){
                    synchronized (object){
                        if(isQuit!=0){
                            break;
                        }
                    }
                }
                System.out.println("t1线程退出");
            });

            Thread t2=new Thread(()->{
                System.out.println("请输入isQuit: ");
                Scanner scanner=new Scanner(System.in);
                //一旦用户输入的值,不为0,此时就会使t1线程执行结束
                isQuit=scanner.nextInt();
            });
            t1.start();
            t2.start();
        }
}


🎈Volatile与Synchronized比较

  • Volatile是轻量级的synchronized,因为它不会引起上下文的切换和调度,所以Volatile性能更好。
  • Volatile只能修饰变量,synchronized可以修饰方法,静态方法,代码块。
  • Volatile对任意单个变量的读/写具有原子性,但是类似于i++这种复合操作不具有原子性。而锁的互斥执行的特性可以确保对整个临界区代码执行具有原子性。
  • 多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
  • volatile是变量在多线程之间的可见性,synchronize是多线程之间访问资源的同步性。

🚩wait和notify

我们之前学的join方法,它是让一个线程执行完之后,再执行另一个线程,这就是哪个线程调用了join,哪个线程就阻塞。

join控制的是结束的先后顺序,但是理想情况下,是希望在结束前,先后顺序的控制。由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知. 但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序。

就比如在打篮球的时候,球场上每个运动员都是独立的"执行流",可以认为是一个"线程“,而完成一个具体的进攻得分动作,则需要多个运动员相互配合,按照一定的顺序执行一定的动作,线程1先"传球",线程2才能"扣球",然后线程1就等待时机。阻塞等待又被唤醒继续执行,这种操作就是再一直的执行程序,而不是先完成一个线程之后第二线程再完成,然后就结束了。

比如,t1和t2两个线程,希望t1先执行,执行的差不多了,在让t2来干.就可以让t2先wait(阻塞,主动放弃cpu),等t1执行的差不多了,再通过notify来通知t2,把t2唤醒,让t2接着干.

  • 使用join,则必须让t1彻底执行完,t2才能运行.如果是希望t1先干50%的活,就让t2开始行动,此时join无能为力.
  • 使用sleep,指定一个休眠的时间.但是t1执行完这些代码,到底花了多少时间,不好估计.
  • 使用wait和notify可以更好的解决上述的问题.

🎈wait()方法

wait进行阻塞, 某个线程调用wait方法,就会进入阻塞(无论是通过哪个对象wait的),此时就处在WAITING状态.

wait,notify和notifyAll这几个类都是Object类的方法,所以Java里随便一个对象,都可以有这三种方法.

  • wait() / wait(long timeout): 让当前线程进入等待状态.
  • notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意: wait, notify, notifyAll 都是 Object 类的方法.

注意,wait也需要这个异常,这个异常,很多带有阻塞功能的方法都带.这些方法都是可以被interrupt方法通过这个异常给唤醒的。后续会再阻塞队列中讲到。

此时抛出异常, 非法的监视器状态异常。监视器是synchronized。
我们首先要知道 wait在执行的时候要进行三步骤:
  • 1.释放当前的锁
  • 2.让线程进入阻塞
  • 3.当线程被唤醒的时候,重新获取到锁
但是首先我们在执行这段代码的时候,我们是释放谁的锁呢?synchronized加锁其实就是把对象头的标记进行操作了, 释放锁的前提是加锁。就比如找工作,你再学校中,学校不让我出去找工作,所以我就不找了,但是前提是你得找到工作了,你才有选择去不去的选择,没有拿到offer之前就想着拒绝去。还比如,一个男生追一个女生,还没追到手都想到了以后和他在一起后孩子的名字都想好了,前提是你得追到手啊,追不到手你取再多名字都不行。
所以我们要让wait放进synchronized锁里面调用,这样就可以确保wait拿到了锁,你才有释放锁的能力。
public class wait_notify_test {
    public static void main(String[] args) throws InterruptedException {
        Object object=new Object();
        synchronized (object){
            System.out.println("wait之前");
           // 将wait放到synchronized里面调用,保证确实拿到了这个锁,才能释放锁
            object.wait();
            System.out.println("wait之后");
        }
    }
}

此时没有报错现象,打印了wait之前代码后,调用wait之后,程序就进入了阻塞状态,因为wait()这种方法无参的是保持死等待的 ,只有等到notify()唤醒才可以执行wait()方法后的程序。
wait 结束等待的条件:
  • 其他线程调用该对象的 notify 方法.
  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.

💻wait(参数)方法

wait除了默认的无参数版本之外,还有一个带参数的版本。带参数的版本就是指定超时时间,避免wait无休止的等待时间,等到一定的时间,就不会再等待了。

public class notify_wait_test2 {
    public static void main(String[] args) {
        Object object=new Object();
        Thread t1=new Thread(()->{
            synchronized (object){
                System.out.println("wait之前");
                try {
                    object.wait(5000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("wait之后");
            }
        });
        t1.start();
    }
}


🎈notify()方法

我们设计下面的代码,线程t1进行wait(),线程t2进行唤醒wait(),因为wait的唤醒需要其他线程调用该对象的notify方法.首先t2线程睡眠3s,让ti线程阻塞等待一会,之后notify()唤醒了wait(),就开始进行wait()方法后的程序了。

public class notify_wait_test2 {
    public static void main(String[] args) {
        Object object=new Object();
        Thread t1=new Thread(()->{
            synchronized (object){
                System.out.println("wait之前");
                try {
                    object.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 (object){
                System.out.println("进行通知之前");
                object.notify();
            }
        });
        t1.start();
        t2.start();
    }
}


🎈解决线程饿死方式

就拿ATM机来举例子,1号滑稽进去之后,就要取钱,发现ATM里面没钱了,取不了,当1号滑稽释放锁之后,此时其他滑稽开始尝试竞争这个锁,但是刚才的1号滑稽,也能参与竞争这个锁。

所以每次都是1号滑稽进去之后,取不了钱,然后又进去,又取不了钱,又进去,其他线程等待锁,都是阻塞状态,没在cpu上执行,当1号滑稽释放锁之后,这些滑稽想去cpu,还需要有一个系统调度的过程,而1号自身,已经在cpu上执行,没有这个调度的过程了,1号近水楼台先得月,更容易拿到锁得。这就导致了一直是1号滑稽进入ATM机中,循环此处,每次都是取不了钱,但是还是1号滑稽占用了这个线程,这样长此以往就形成了”线程饿死“的状态。

针对上述情况,同样可以使用wait和notify解决,让1号滑稽,在发现没钱的时候,就进行wait(wait内部本身就会释放锁,并且进入阻塞),1号滑稽就不会参与后续的锁竞争了,也把锁释放出来让别人获取。就给其他的滑稽提供了机会了。
wait的过程是等,等待运钞车把钱送过来,运钞车的线程就相当于调用了notify唤醒的线程,这个等的过程,是阻塞的,但是不会占据cpu。

🎈notifyAll()方法

        notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程. 范例:使用notifyAll() 方法唤醒所有等待线程 , 在上面的代码基础上做出修改
        调用wait不一定就只有一个线程调用,N个线程都可以调用wait,此时,当有多个线程调用的时候,这些线程都会进入阻塞状态。

        唤醒的方式就有2种方法。notifyAll唤醒的时候,wait要涉及到一个重新获取锁的过程,也是需要串行执行的而并不是并行执行。虽然提供了notifyAll,相比之下notify更可控,用的更多一些。


🚩wait 和 sleep 的对比(面试题)

其实理论上 wait sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间。
唯一的相同点就是都可以让线程放弃执行一段时间.
当然为了面试的目的,我们还是总结下:
  • 1. wait 需要搭配 synchronized 使用. sleep 不需要.
  • 2. wait Object 的方法 sleep Thread 的静态方法

人拥有可以反复尝试的自由,也拥有停步或者回头的权利。

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

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

相关文章

【Python + Django】静态文件的添加

前言&#xff1a; 前一篇文章我们已经学会了怎么用django写文本页面啦&#xff01;&#xff01;&#xff01; 有一说一&#xff0c;这个静态页面是真的丑。 我们总得用一些花花绿绿的东西把这个丑陋的网站给装饰一下吧&#xff01;&#xff01;&#xff01;&#xff01;&…

java中异常类

异常 异常体系继承结构 Throwable类是 Java 语言中所有错误或异常的超类&#xff0c;只有当对象是此类&#xff08;或其子类之一&#xff09;的实例时&#xff0c;才能通过 Java 虚拟机或者 Java throw 语句抛出。     异常是对象&#xff0c;而对象都采用类来定义。异常的…

IDEA使用手册

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

真机笔记(1)第一阶段知识讲解

目录 第一阶段讲解&#xff1a; 1.1 机房 1.2 分类&#xff1a; 1.3 机房建设标准 1.3.1 安全性: 1.3.2 供电&#xff1a; 1.3.3 空气调节&#xff1a;&#xff08;恒温恒湿&#xff09; 1.3.4 电磁防护&#xff1a; 2.1 机柜 2.2 分类 2.3 机柜的高度单位 3.1 设备…

postman 用上一个请求的响应体中的字段设置下一个请求的请求参数

文章目录 IntroPostman usagePre-request ScriptTests javascripts API Intro 这一切都是为了增加自动化动作所占的比例&#xff08;减少人手工操作复制粘贴可能会造成的错误&#xff09;。 Postman usage 最常用的&#xff1a;选HTTP方法类型、写URL&#xff0c;在Headers中…

1240. 完全二叉树的权值

给定一棵包含 N 个节点的完全二叉树&#xff0c;树上每个节点都有一个权值&#xff0c;按从上到下、从左到右的顺序依次是 A1,A2,⋅⋅⋅AN&#xff0c;如下图所示&#xff1a; 现在小明要把相同深度的节点的权值加在一起&#xff0c;他想知道哪个深度的节点权值之和最大&#x…

外包干了6天,技术退步明显。。。。

说一下自己的情况&#xff0c;本科生&#xff0c;19年通过校招进入上海某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试&a…

【Linux命令】查看内存占用情况(mem, swap)

1. 方法1&#xff08;top&#xff09; # top2.方法2&#xff08;free&#xff09; # free -h3. 方法3&#xff08;swapon&#xff09; # swapon -s

【技巧】ChatGPT Prompt 提示语大全

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 主要来自&#xff1a;https://github.com/f/awesome-chatgpt-prompts ChatGPT SEO提示 Contributed by: StoryChief AI Reference: 7 Powerful ChatGPT Prompts to Create SEO Content Faster 供稿人&#xff1a;…

STM32 CubeMx创建Lwip+FreeRtos时出现ping不通的问题

STM32 CubeMx创建LwipFreeRtos时出现ping不通 1、配置ETH&#xff0c;使用中断 2、配置Lwip&#xff08;使用静态ip&#xff09;&#xff0c;其余什么都不用管 3、配置FreeRtos&#xff08;选择V2版本&#xff09;&#xff0c;其余什么都不用管 4、创建代码 5、查看自动生…

在虚拟机上安装ubuntu

初学者建议选择“清除整个磁盘并安装ubuntu”&#xff0c;下面那个选项留给熟练使用虚拟机后的自己吧&#xff0c;哈哈。需要了解的是&#xff0c;“清除整个磁盘并安装ubuntu”此选项仅仅会清除你这个虚拟机分配的磁盘&#xff0c;不会影响你电脑上的其它虚拟机&#xff0c;哪…

深度学习,CRNN+CTC和Attention OCR你更青睐哪一种?

深度学习在OCR领域的应用已经取得了瞩目的成果&#xff0c;而选择合适的算法对于提升OCR的识别准确率至关重要。在众多算法中&#xff0c;CRNN和Attention OCR犹如两颗璀璨的明珠&#xff0c;备受瞩目。 CRNN&#xff0c;这位结合了卷积神经网络&#xff08;CNN&#xff09;和…

UE5 GameMode C++函数 学习

已经尝试&#xff0c;确实能重启游戏 类描述符加了noplaceable过后即使是Actor也不能放到场景中了&#xff0c;关卡蓝图&#xff0c;GameMode&#xff0c;GameState这些就不能放场景中了 UFUNCTION(exec)

lora-scripts 训练IP形象

CodeWithGPU | 能复现才是好算法CodeWithGPU | GitHub AI算法复现社区&#xff0c;能复现才是好算法https://www.codewithgpu.com/i/Akegarasu/lora-scripts/lora-trainstable-diffusion打造自己的lora模型&#xff08;使用lora-scripts&#xff09;-CSDN博客文章浏览阅读1.1k次…

C/C++在线参考手册的使用技巧

cppreference.com是一个在线的C/C参考手册&#xff0c;是C/C学习者最常用的网站。 网址&#xff1a;cppreference.com 1&#xff0e;搜索 不知道为什么这个网站总是不能正常搜索&#xff0c;实在是太不方便了。 有两个退而求其次的方法&#xff1a; (1)通过搜索引擎指定域名…

【Netty】TCP粘包、拆包、编解码问题

TCP粘包、拆包、编解码问题 UserInfo userInfo1new UserInfo();ByteBuf buf Unpooled.copiedBuffer(userInfo1.toString().getBytes(StandardCharsets.UTF_8));UserInfo userInfo1new UserInfo(); 这行代码创建了一个新的UserInfo对象&#xff0c;并将其引用存储在名为userInf…

基于SpringBoot医院管理系统

采用技术 基于SpringBoot医院管理系统的设计与实现~ 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBootMyBatis 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 页面展示效果 医生模块 病床信息管理 药房信息管理 护士模块 个人中心管理 …

DS-红黑树(RBTree)

一.红黑树 1.1 红黑树的起源 当对对AVL树做一些结构修改的操作时候&#xff0c;性能较为低下&#xff0c;比如&#xff1a;插入时要维护其绝对平衡&#xff0c;旋转的次数比较多&#xff0c;更差的是在删除时&#xff0c;有可能一直要让旋转持续到根的位置。 因此1972年Rudolf…

循环冗余校验CRC和FPGA实现

一、概念 CRC校验&#xff0c;中文翻译过来是&#xff1a;循环冗余校验&#xff0c;英文全称是&#xff1a;Cyclic Redundancy Check。是一种通过对数据产生固定位数的校验码&#xff0c;以检验数据是否存在错误的技术。 其主要特点是检错能力强、开销小&#xff0c;易于电路实…

设计模式 --4:工厂方法模式

总结 &#xff1a; 个人理解&#xff1a; 工厂方法模式就是在简单工程模式的基础下将工厂类抽象出来。如果不抽象工厂类 &#xff0c;每一次创建一个新的算法&#xff0c;都要修改原来的工厂类&#xff0c;这不符合 开放–封闭原则 将工厂类给抽象出来&#xff0c;让具体的算法…