volatile wait notify

volatile

volatile 这个关键字和内存可见性问题是密切相关的, 这里用一段代码来演示内存可见性问题

import java.util.Scanner;

class MyCounter {
    public int flag = 0;
}

public class ThreadDemo15 {
    public static void main(String[] args) {
        MyCounter counter = new MyCounter();

        Thread t1 = new Thread(() -> {
            while (counter.flag == 0) {

            }
            System.out.println("t1 循环结束");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数: ");
            counter.flag = scanner.nextInt();
        });

        t1.start();
        t2.start();
    }
}

上述代码定义了一个 MyCounter 类, 这个类中有一个 flag 变量, 初始值为 0.
在 main 方法中实例化了 MyCounter 类, 并且创建了两个线程 t1 和 t2.

  • 在 t1 线程中有一个 while 循环, 循环条件是 counter.flag == 0
  • 在 t2 线程中让用户在控制台中输入一个整数, 在输入完之后将这个整数赋值给 flag
  • 这个案例的预期结果为: 当 t2 读取控制台中的整数之后将值赋给 flag 变量, t1 线程中的循环检测 flag 的值不为 0 的时候, t1 结束循环.

但是当运行了这个案例, 输入了一个整数之后, 发现 t1 线程并没有从循环中走出来

这明显与预期结果不符合. 这个情况, 就叫做 内存可见性问题

  • 我们可以来分析一下 t1 线程中所进行的操作

在这个循环中, 按照汇编的角度来理解, 主要会进行两步操作

  1. load, 把内存中的 flag 值读取到寄存器中
  2. cmp, 把寄存器中的值与 0 进行比较, 根据比较的结果, 决定程序下一步该怎么执行

站在计算机的角度来看这个 while 循环, 当程序启动的时候, 这个循环一秒就能执行 数百万次 以上
在计算机的视角中, load 和 cmp 这两个操作所执行的速度相对比, load 的速度会慢非常多, 由于 load 执行的速度太慢了, 再加上反复 load 多次的结果都是一样的, 这时 JVM 就做出一个大胆的决定: 它判定没人会修改 flag 变量的值, 就不再进行重复 load 的操作了, 干脆只读取一次就好… (这个行为就是编译器优化)

  • 由于 JVM 在多线程环境下对于这种情况的判断可能存在误差, 在编译器优化的时候就改变了代码的预期结果, 这就引来了内存可见性问题

而想要实际解决这个问题, 就要程序员手动干预了, 可以给 flag 变量加上 volatile 关键字, 就是在告诉编译器: 这个变量是易变的, 让 JVM 不敢对这个易变的变量进行激进的优化

class MyCounter {
    public volatile int flag = 0;
}

再次运行, 结果和预期相符

wait & notify

在多线程环境下, 线程最大的问题就是: 抢占式执行, 随机调度. 所以各个线程之间执行的顺序就是由操作系统来调度的, 但是在实际开发中, 往往需要让程序按照指定的顺序进行运行.
结合之前学习的 join 和 sleep 让线程阻塞或者休眠也能在一定程度上固定线程执行的顺序, 但是在复杂的场景中, 就会显得有些无力…
但是配合上 wait 和 notify, 就能很好的解决上述问题
wait 和 notify 方法都是 Object 类中的方法

wait

wait 方法的作用是进行阻塞
当某个线程调用了 wait 方法的时候, 就会进入阻塞 (WAITING) 状态

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

使用 wait 方法, 要抛出 InterruptedException 异常, 这个异常很多带有阻塞功能的方法都带 (都是可以被 interrupt 方法通过这个异常来唤醒)

wait 方法不带上任何参数, 就是死等, 等待到有其他的线程来将它唤醒
先运行一下这个程序

发现编译器抛出了一个异常 (非法锁状态异常), 为什么会有这个异常呢? 让我们来了解一下 wait 方法是做什么的:

  1. 先释放锁
  2. 进行阻塞等待
  3. 收到通知后, 重新尝试获取锁, 并且在获取到锁之后, 继续往下执行

在了解 wait 要进行的操作之后, 就能发现上述程序中一个很重要的问题: 那就是 “没有锁”
这边都没给 wait 加锁, 还谈何释放锁, 所以编译器就会抛出这样一个异常

所以, 很重要的一点就是:
在 Java 中, wait 需要搭配 synchronized 来使用

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

可以看到运行结果

  • 控制台中打印了 “wait 之前” 之后, 调用了 wait 方法, 使 main 线程进行了阻塞操作, 那么要如何唤醒这个 wait 方法并执行下一步呢?

接下来介绍一下 notify 关键字

notify

notify 关键字用来唤醒被 wait 阻塞的线程. 该方法也必须在 synchronized 代码块中才能使用

public class ThreadDemo17 {
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();

        Thread t1 = new Thread(() -> {
           // 这个线程负责进行等待 
            System.out.println("t1: wait 之前");
            try {
                synchronized (o) {
                    o.wait();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("t1: wait 之后");
        });

        Thread t2 = new Thread(() -> {
            System.out.println("t2: notify 之前");
            synchronized (o) {
                o.notify();
            }
            System.out.println("t2: notify 之后");
        });

        t1.start();
        Thread.sleep(1000);
        t2.start();
    }
}

注意:
调用 wait 和 notify 的两个对象必须是同一个对象, 并且两个方法外围包裹的 synchronized 代码块所指定的锁对象也必须相同, 这样 notify 才能正确的唤醒阻塞这个线程的 wait 方法

运行程序查看结果

可以看到, 先调用 wait 使线程阻塞, 在 notify 之后就能唤醒对应的 wait 所阻塞的线程

但是在一个大型项目中, 程序的执行逻辑非常复杂, 在这种背景下, 程序中的 wait 可能会出现没有 notify 来唤醒它的情况, 这就导致这个线程会被一直阻塞在那里, 无法被操作系统进行调度.

为了解决上面的场景, wait 提供了两个版本的方法

方法作用
wait()阻塞当前线程, 并且只能被 notify 方法所唤醒
wait(long miles)指定了最大等待时间, 阻塞当前线程, 可以被 notify 方法唤醒, 或者阻塞到等待的最大时间之后自动唤醒
notifyAll

notify 方法是唤醒一个呗同一个对象所调用的 wait 方法所阻塞的线程, 但是当有多个线程被同一个对象所调用的 wait 方法阻塞的时候, 此时执行 notify 方法就会随机唤醒其中的某一个线程.
此时若想要唤醒所有的被同一对象所阻塞的线程, 就可以使用 notifyAll 方法

当有多个线程被同时唤醒, 这些线程都需要重新去竞争锁

wait 和 sleep 的区别
  • 相同点:
    都可以让一个线程进入阻塞状态
  • 不同点:
  1. wait 是 Object 类中的方法, join 是 Thread 类的类方法
  2. wait 需要搭配锁 (synchronized) 来使用, 而 sleep 不需要
  3. wait 被调用后当前线程进入 BLOCKED 状态并释放锁, 并可以通过 notify 和 notifyAll 方法进行唤醒, 而 sleep 被调用后当前线程进入 TIMED_WAITING 状态, 不涉及锁相关的操作
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值