JAVA——volatile,wait,notife

volatile关键字

volatile关键字可以保证内存的可见性,什么是内存的可见性呢?这时候我们需要先想明白一个地方就是内存是可见的吗?我们能直接看到我们写的一个变量或者申请的一块儿内存来自于那里吗?很明显不知道,所以在此我们要先搞明白jvm的内存模型

简识jvm内存模型

在这里插入图片描述
从这张图种我们可以清除的看到其实我们的创建的线程在工作的时候,我们的数据并不是直接加到主内存中的,这里的主内存我们可以理解成硬盘内存,而工作内存其实就是jvm为每个线程独立开辟的一块内存,而线程在工作中所产生的各种变量啊资源啊其实并不是直接加载到主内存中的而是先加载到工作内存中在加载到主内存中,而工作内存其实就是在我们的cpu和寄存器中临时开辟的一块儿内存。
举个例子
我们来举个例子帮助大家理解一下现在假如说我们有一个java线程t1线程,这个t1有一块属于自己的工作内存,那么当我们的t1线程创建一个变量叫做x然后这个x的一个历程应该是先加载到工作内存再由工作内存转存到主内存中,那么当我们突然需要对x进行修改呢?其实就是先把主内存中x读取出来然后交给t1线程进行修改,修改过后再重新更新到主内存中。

我们可以知道工作内存和主内存是独立的,各个工作内存之间也是独立的,当线程创建或者修改某个变量时将会将这个变量传递给工作内存再由工作内存加载到主内存中。

内存上的优化

可是这里面有一个问题就是我们在主内存中读取或者修改某个变量都是非常麻烦的,因此为了避免这些麻烦呢jvm做的有个优化那就是,当jvm发现某个变量短时间内进行了大量的修改或者访问的时候那么jvm就不会把这个变量加载到主内存中而是先把这个变量加载到工作内存中。以此来避免时间的浪费,但是一个问题的解决有时也会诞生新的问题。

问题的产生

当我们是单线程的时候上述优化可以说是非常的优秀,但是如果是多线程呢?比如说下面这个例子

import java.util.Scanner;

public class Main {
    static class Counter {
        public int flag = 0;
    }
    public static void main(String[] args) {
        Counter c=new Counter();
        Thread t1=new Thread(()->{
           while(c.flag==0){
               
           }
        });
        Scanner scan=new Scanner(System.in);
        Thread t2=new Thread(()->{
            System.out.println("输入一个整数:");
            c.flag=scan.nextInt();
        });
        t1.start();
        t2.start();

    }
}

对于上面的这个代码我们来看一下运行截图
在这里插入图片描述
请看当我们输入非0的时候代码并没有像我们预想中的那样结束相反一直在运行这是为什么呢?因为我们刚刚说了当一个线程需要使用某个资源的时候会先从主内存中申请而,一个线程改变某个资源的时候也会查看这个资源是否被多次调用如果是的话那就会先暂时不把他存入主内存中,这里t1线程不断的查看当前变量的值导致jvm认为这个变量如果直接加载到内存中会相当的影响程序的效率因此关于这个变量的修改等都会先存入工作内存中,这就导致我们虽然输入了1但是这个1不会被t1线程查询到因为这个值根本没有写入主内存中。那么解决办法就是用volatile。

volatile的作用

它的作用其实就是让被修饰的变量在被修改后直接存入主内存中。

代码被volatile修饰后:

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存

代码读取volatile修饰的变量:

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

这里我们可以看到volatile的作用其实就是让jvm不要进行所谓的优化,我们对某个变量的修改或者访问将直接让工作内存重新从主内存中获取,从而避免了上述情况的发生。

wait()

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知.
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.
这里给大家举个例子比如说我们在食堂打饭,是要进行排队的此时没有排到的同学需要站在后面进行等待,而对于线程来说由于是抢占式执行因此当我们不管的时候其实更像是下面的那个图片很多人在那里围着抢饭分不出来谁先谁后,谁先抢到谁就能吃上,因此是比较混乱的,在实际开发中我们希望我们的线程调度是可以有一定管理的,尤其是当面对一些突发情况的时候,比如说我们在ATM机排队办理业务,当此时正在办理取钱的业务的线程发现ATM里面没有钱这时候由于他特别没有素质因此抢占着这个ATM机不出来,这时候就会造成后面排队的线程饿死,因此wait()方法的作用这时候就体现出来了那就是把这种线程给暂停掉执行。
在这里插入图片描述
在这里插入图片描述

wait()的作用

关于wait的作用主要分为以下三点

wait做的事情:
使当前正在运行代码的线程进行等待(把线程放进等待队列中)
释放当前的锁
满足一定条件时被唤醒并且重新获取这个锁

这里我们要明白一个事情那就是wait的工作中是会释放其拥有的锁的,因此我们要先保证这个线程已经拥有了锁。

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

wait结束的条件

  • 其他线程调用该对象的 notify 方法.
  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出InterruptedException 异常.

那么接下来我们来使用以下notify方法

notify()

notify方法是用来唤醒等待的线程那么他是如何唤醒的呢我们来看一下以下代码

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
       Object locker=new Object();
       Thread t1=new Thread(()->{
           synchronized (locker){
               System.out.println("开始等待");
               try {
                   locker.wait();
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
               System.out.println("已被唤醒");
           }
       });
       Thread t2=new Thread(()->{
           synchronized (locker){
               System.out.println("开始唤醒");
               locker.notify();
               System.out.println("唤醒成功");
           }
       });
       t1.start();
       t2.start();
        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

    }
}

我们来看一下运行截图
在这里插入图片描述
这里我们可以看到当t1线程等待后会暂停代码的执行当t2线程唤醒之后t1线程会继续自己还未完成的代码继续执行,而不是重新执行。
但是这里我们要明白一个问题就是当t1线程被唤醒后是否需要重新获取锁呢?如果需要的话那么这是怎样的一个打印顺序呢?从上面的代码截图可以看出来那就是t2线程此时也获取了lcoker锁因此肯定不可能因为t1现成的唤醒而直接释放自己的锁从而让t1继续运行而是会选择当自己的代码运行完毕后再释放锁让t1继续执行。我们可以用下面的代码进行实验

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
       Object locker=new Object();
       Thread t1=new Thread(()->{
           synchronized (locker){
               System.out.println("开始等待");
               try {
                   locker.wait();
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
               System.out.println("已被唤醒");
           }
       });
       Thread t2=new Thread(()->{
           synchronized (locker){
               System.out.println("开始唤醒");
               locker.notify();
               while(true){
                   synchronized (locker){
                       try {
                           Thread.sleep(1000);
                       } catch (InterruptedException e) {
                           throw new RuntimeException(e);
                       }
                       System.out.println("不释放锁");
                   }
               }
           }
       });
       t1.start();
       t2.start();
        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

    }
}

在这里插入图片描述
这里我们可以看到即使我们唤醒了t1线程但是当我们不释放锁的时候t1线程也不能继续运行。

notify的唤醒顺序

从官方的文档来看notify其实是乱序唤醒的是由JVM内部的调度机制决定的,至于究竟是什么调度机制呢?我也不清楚,但是我们可以思考一个事情就是如果说我们唤醒一个线程后就让notify线程直接释放自己的锁,那么这时候释放的顺序会是什么样子呢?我的运行结果是顺序的大家也可以下去尝试一下。

	努力学习和工作和爱的人并肩看夕阳
  • 27
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值