volatile,wait与notify

本篇主要介绍几个常用的关键字和方法,volatile,wait和notify

一、volatile

在java中,volatile关键字主要用来解决两个问题,内存可见性和指令重排序。下面我们来具体看一下这两个问题。

内存可见性

在jvm中,为了更好的区分内存与cpu的关系,将内存分为了两种,一种是工作内存,一种是主内存

工作内存:cpu上的寄存器和缓存

主内存:我们平常所理解的内存条上的内存

在有了这两个概念基础后,我们再来看一下什么是内存可见性

内存可见性:当一个线程对主内存中的共享数据进行修改后,其他线程能够感知到这个数据的变化

 正常来说,由于这个共享数据每一个线程都能够进行访问,那是不是任何时候都具有内存可见性呢?我们先来看一下下面的代码

public class VolatileAndNotifyAndWait {
    //创建共享变量flag
    static int flag = 0;
    public static void test01(){
        //创建两个线程
        Thread t1 = new Thread(() -> {
            //判断flag的值是否为0,为0就一直循环
            while (flag == 0){

            }
        });
        Thread t2 = new Thread(() -> {
            //休眠一会
            try {
                Thread.sleep(5000);
                //将flag的值改为1
                flag = 1;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();
        t2.start();
    }

    public static void main(String[] args) {
        test01();
    }

}

运行结果

通过运行结果发现,进程一直未结束,进程未结束说明仍然还有前台线程在运行,让我们通过jconsle来看一下具体是哪个线程还在运行

通过jconsle我们发现,线程Thread-0任然处与RUNNABLE状态,并且正在执行第8行代码,而第8行正是线程t1 的循环操作,再结合线程一直未结束我们可以得出一个结论,这个循环一直没有结束,但通过线程t2执行的任务我们可以发现flag的值已经被改为1了,这时循环条件已经不成立了,那为什么循环没有结束呢?

首先我们来看一下flag == 1这串代码的执行过程

1.将flag的值读到工作内存

2.将工作内存的值与与1比较,判断是否继续执行

然后我们再观察一下前面的代码,t2线程在修改flag的值之前,先休眠了五秒钟,在这段时间,t1线程将会不断重复上面的流程,直到flag的值发生变化,这个过程中,将flag读到工作内存的这个过程,相对而言是比较耗时的,此时,为了提高程序的性能,jvm就会自动的进行优化,不再往主内存读取flag的值,而是直接采用工作内存的值进行比较判断,这样就导致主内存中flag的值不可见了,即使线程t2修改了flag的值,循环也不会结束。

通过给变量修饰volatile关键字就可以解决上述问题。经过volatile修饰,就相当于告诉jvm这个变量是可变的,所以每次在读取这个变量的值时都只能从主内存中读取,而不能从工作内存中读取。这样就保证这个变量的每一次操作都是具有内存可见性的,从而就避免了上述问题。对前面的代码中的flag用volatile修饰后,可以发现代码已经能够正常结束了

指令重排序

指令重排序 : 指令重排序是指cpu或者编译器为了优化程序执行的性能,将某些指令的执行顺序进行重新排序的过程

例如,我们在给一个引用变量进行赋值时,正常的执行顺序是

1.开辟空间

2.初始化空间

3.将空间赋给变量

但在指令重排序后可能变为

1.开辟空间

2.将空间赋给变量

3.初始化空间

这种顺序的变化通常不会影响我们代码的执行结果,但也不排除某些特殊情况的出现,所以为了避免这种潜在的隐患,我们也可以选择用volatile来进行修饰,因为volatile还具有让指令以正常顺序执行的作用。

总的来说,使用volatile关键字可以解决内存可见性和指令重排序的问题,但volatile并不能保原子性,要保证原子性,还是得使用synchronize

二wait与notify

wait

前面我们了解过两种让线程暂时停止执行的方式,一种是join方法,一种是sleep方法,但这两种方法使用起来都不够灵活,那有没有一种更加灵活的方法呢,这时我们就需要引入wait方法了。wait方法的执行流程如下

1.释放锁(这里的锁对象与调用wait方法的对象是同一个)

2.进行阻塞等待

3.被唤醒后重新获取锁,获取成功后进行执行下面的代码

 由于wait方法需要执行释放锁的操作,所以wait方法一般与synchronize搭配使用,那如果单独使用会怎么样呢,我们通过代码看一下

public static void test02(){
        //创建对象
       Object o = new Object();
        try {
            //调用wait方法
            o.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

运行结果

通过上述代码和运行结果我们发现,调用 wait方法需要处理一个中断异常,并且还抛出了一个非法锁异常,为什么会抛出这个异常其实也很好理解,wait会释放锁,但这里又没有锁可以释放,自然会出现问题了,接下来我们来看看wait与synchronize搭配使用后的代码

 public static void test02(){
        //创建对象
       Object o = new Object();
        try {
            synchronized (o) {
                //调用wait方法
                o.wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

运行结果

程序没有报错了,但程序却卡在这了,为什么会这样呢?前面我们说过调用wait方法的线程在释放锁后会阻塞等待,所以这里程序并不会结束,而是卡在了这里,此时我们就需要使用另一个方法来唤醒正在等待的线程了

notify

使用notify方法就能够唤醒在wait的线程了,notify方法与wait方法一样也是要搭配synchronize关键字使用,因为notify只能唤醒同一个锁对象上等待的线程。将前面的代码引入notify方法

public static void test02(){
        //创建对象
       Object o = new Object();
        //创建另一个线程进行唤醒
        Thread t = new Thread(() ->{
            //先休眠一会,以免notify先与wait执行
            try {
                Thread.sleep(1000);
                synchronized (o){
                    o.notify();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
           t.start();
        try {
            synchronized (o) {
                //调用wait方法
                o.wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

运行结果

这次程序就能正常结束了。

如果一直没有线程来调用notify方法,难道就只能一直死等吗,这显然不是我们想看到的,为了不让我们的线程一直死等,我们可以使用带参数的wait方法

void wait(long miles)

此处的参数就代表者最大等待时间,超过这个时间就会被自动唤醒

notify方法在设定上是只能一次随机唤醒一个线程的,那如果要一次唤醒全部的线程该怎么办呢,这时我们可以使用notifyAll方法,notifyAll方法能够一次唤醒全部在同一个锁对象上等待的线程

如果在调用notify方法时没有一个线程在等待呢,这时其实啥也不会发生

最后要注意的是notify和wait这都种方法是Object类中的方法,这也就意味着所有对象都能够调用这两个方法

三、实战:多线程实现打印"ABC"

在前面的篇章中我们知道线程的执行顺序是不确定的,但如果我们能灵活的使用notify和wait方法,就能够在一定程度上控制线程执行的顺序,让线程有序,下面我们就来通过对wait和notify方法的使用来实现一下在多线程环境下打印“ABC"吧

public static void test03(){
        //创建两个锁对象
        Object o1 = new Object();
        Object o2 = new Object();
        //创建线程1负责打印’A'
        Thread t1 = new Thread(() ->{
            //对o1加锁
            synchronized (o1){
                //拿到锁直接打印'A'
                System.out.println('A');
                //在唤醒在o1下等待的线程
                o1.notify();
            }
        });
        //创建线程2,打印'B'
        Thread t2 = new Thread(() ->{
            //获取锁o1
            synchronized (o1){
                try {
                    o1.wait();
                    //被唤醒后打印'B’
                    System.out.println('B');
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //获取锁o2
            synchronized (o2){
                //唤醒o2下的线程
                o2.notify();
            }
        });
        //创建线程3打印‘C’
        Thread t3 = new Thread(() -> {
            //获取锁o2
            synchronized (o2){
                try {
                    o2.wait();
                    //唤醒后打印‘C'
                    System.out.println('C');
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t2.start();
        t3.start();
        //先休眠一会,以免线程1先或得锁从而使notify无效
        t1.start();

    }




    public static void main(String[] args) {
        test03();
        // test01();
    }

运行结果

像这样notify和wait方法的运用场景还有很多,如果我们能够熟练灵活的使用这两种方法的话,不仅能够能够解决很多开发过程中的问题,还能够提高我们的开发效率,所以多加练习吧!

  • 23
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值