> 文章列表 > JavaEE——volatile、wait、notify三个关键字的解释

JavaEE——volatile、wait、notify三个关键字的解释

JavaEE——volatile、wait、notify三个关键字的解释

文章目录

  • 一、volatile和内存可见性
    • 1.解释内存可见性问题
    • 2. volatile 的使用与相关问题
  • 二、wait 和 notify
    • 1.wait 方法
    • 2.notify() 方法
    • 3. 关于 notifyAll() 方法
    • 4. wait 和 sleep 之间的简单比较

一、volatile和内存可见性

前面的文章,我们已经提及到了内存可见性问题,这里在对内存可见性进行简单的描述:内存可见性是指,一个线程对共享变量值的修改,可以被其他线程及时的看到。

1.解释内存可见性问题

对于内存可见性问题,我们已经知道,出现问题的原因在与,一个线程针对一个变量进行读取,同时另一个线程针对这个变量进行修改,此时读到的值不一定就是修改后的值。

下面我通过一个简单的代码来展示一下这个问题:

代码示例:

import java.util.Scanner;class MyCounter{int flag = 0;
}
//通过两个线程对一个元素进行读取和修改操作展现问题
public class ThreadDemo {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 scanner = new Scanner(System.in);System.out.println("请输入一个数字");myCounter.flag = scanner.nextInt();});t1.start();t2.start();}
}

运行结果:
JavaEE——volatile、wait、notify三个关键字的解释
可以发现输入数字改变 flag 的值后,代码没有停止运行。此时我们借助 jconsole 工具来看一下:
JavaEE——volatile、wait、notify三个关键字的解释
如上图所示,在输入数据前,两个线程的状态。
JavaEE——volatile、wait、notify三个关键字的解释
在输入数据后,t2 线程消失不见,只剩 t1 线程进行死循环。

在这里我们肯定会有一个疑问,不是已经将 flag 的值修改了,当线程 t1 再次获取的时候应该跳出循环,但是为何仍然出现了死循环。

这里使用汇编来理解,大概分为以下两点:

  1. load,将内存中 flag 的值获取到寄存器中
  2. cmp,将寄存器中的值和 0 进行比较,决定下一步如何执行。

上面的两个操作,是循环的一个整体,这个循环的速度极快,大约在 1 秒钟百万次以上。
在 CPU 对寄存器的读取操作上,速度也是要比计算器对内存读取的速度快很多倍,所以,load 操作和 cmp 操作相比,速度会慢非常多。
正是因为上面的种种原因,导致反复 load 到的结果都一样,对此 JVM 做出了一个大胆的决定,不在多次获取 flag 判定没有修改 flag 的值。(这也是编译器优化的一种方式)

2. volatile 的使用与相关问题

  1. volatile 关键字的使用

通过上面的问题的描述,呢么要解决这个问题只能靠程序员手动进行干预。volatile 这个关键字就是干预的关键所在。

给上面代码中的 flag 变量前加上 volition 关键字进行修饰,表达的意思是告诉编译器,这个变量是 “易变” 的,要求编译器每次都要进行读取操作。

代码示例:

import java.util.Scanner;class MyCounter{
// 添加 volatile 关键字修饰 flagvolatile 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 scanner = new Scanner(System.in);System.out.println("请输入一个数字");myCounter.flag = scanner.nextInt();});t1.start();t2.start();}
}

运行结果:
JavaEE——volatile、wait、notify三个关键字的解释
注:volatile 关键字只能修饰变量,不能修饰方法。

  1. volatile 关键字不保证原子性

关于这一点,我们用 synchronized 修饰的代码替换后来展示一下。

代码展示:

class Counter{//将原先 synchronized 关键字修改的替换成 volatile 关键字public volatile int count = 0;public void increase(){count++;}
}public class ThreadDemo {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);}
}

上面的代码被 synchronized 关键字修改后可以计算出正确的数值 10万
结果展示:
JavaEE——volatile、wait、notify三个关键字的解释
因此,这样就证明了 volatile 关键字并不能保证变量的原子性

二、wait 和 notify

线程的最大问题,就是抢占式执行,随机调度。对此,线程执行的先后顺序就难以预知,但是在实际开发中,我们需要更加合理的处理线程之间的先后顺序。

假设两个线程需要合作来完成一项工作,需要交叉配合执行。那么,使用 join 或者 sleep 可否满足我们的需要?答案是,不行!

  • 对于 join,这个方法必须要 t1 线程执行完毕 t2 才会运行,无法做到交叉配合运行。
  • 对于 sleep,这个方法虽然可以设定一个休眠时间,但是,两个线程之间配合工作,之间等待的时间是复杂且难以计算的,因此也不能使用。

需要注意的是,虽然 wait notify 相较于 join 等功能性更强,但是使用也相对会比较复杂。
注:wait,notify,notifyAll 都是 Object 类的方法。

1.wait 方法

wait 进行阻塞,当某个线程调用 wait 方法,这个线程就会处在阻塞状态 ( wait() 不加参数,就是一个“死等”,一直等待,直到有其他线程唤醒)

wait 的相关操作:

  • 使当前执行代码的线程进行等待。
  • 释放当前的锁。
  • 满足一定条件时被唤醒,重新尝试获取锁。

代码展示:

//直接使用 wait() 方法public static void main(String[] args) throws InterruptedException {Object object = new Object();System.out.println("等待中");object.wait();System.out.println("等待结束");}

运行结果:
JavaEE——volatile、wait、notify三个关键字的解释
如图所示,程序报错,不难发现直接使用 wait() 方法是错误的。

我们需要注意到 wait 的内部操作中有这么一条 —— 先释放当前的锁,所以直接使用就会出现一个锁状态异常这样的情况。

因此 wait 操作需要搭配 synchronized 关键字来使用。先加锁,在解锁,再等待
代码展示:

    public static void main(String[] args) throws InterruptedException {Object object = new Object();synchronized (object){System.out.println("等待中");object.wait();System.out.println("等待结束");}}

结果展示:
JavaEE——volatile、wait、notify三个关键字的解释
程序进入正常的运转。

2.notify() 方法

关于 notify() 方法就是唤醒正在等待的线程。

为了确保 notify() 可以正确通知,需要对正在等待的对象再次进行加锁

notify() 的相关操作:

  • 通知正在的进行等待的线程中的对象,使得该对象重新获取对象锁。
  • 如果多个线程进行等待,则由线程调度器随机调度一个 wait 状态的线程。
  • 在 notify() 方法后,当前线程不会马上释放对象锁,要等到执行 notify() 方法当前所在的代码块执行完毕才会释放对该对象的锁。

代码示例:

    public static void main(String[] args) throws InterruptedException {Object object = new Object();Thread t1 = new Thread(()->{//这个线程负责等待System.out.println("t1 wait之前");synchronized (object){try {//wait 会先释放锁,再将对象挂起等待object.wait();//wait 在被唤醒后会尝试在此获取当前的锁} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1 wait之后");});Thread t2 = new Thread(()->{System.out.println("t2 notify之前");// notify 获取到对应的元素的锁,才能进行通知synchronized (object){object.notify();//验证 notify 后不会直接释放该对象的锁try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("notify 当前代码块的其他代码。。。");}//从这开始就是抢占式执行了try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t2 notify之后");});t1.start();//设定休眠时间确保 t1 等待线程先启动//防止 notify 空打一炮Thread.sleep(1000);t2.start();}

运行结果:
JavaEE——volatile、wait、notify三个关键字的解释

wait 和 notify 两个关键字是组合起来使用的,所以会比较复杂,所以下面我来简单总结一下:

设定对象 A

  1. wait 操作需要先解锁,因此,首先对对象 A 进行加锁
  2. 加锁后的对象会挂起等待,notify 想要通知 A 停止挂起,为了确保 notify作用在同一对象,需要让 notify 获取到 A,因此,需要对 A 再次进行加锁。
  3. 在 notify 关键字之后,并不会立即释放当前 A 对象的锁,当执行完 notify 关键字所在代码块的内容后,对所进行释放。
  4. 释放后,wait 关键字会重新尝试获取关键字 A 的锁,之后继续执行后续代码。

3. 关于 notifyAll() 方法

对于 notifyAll 方法的理解很简单,多个线程 wait 的时候,notify 随机唤醒一个,notifyAll 则是全部唤醒,让这些线程一块竞争锁。

4. wait 和 sleep 之间的简单比较

wait 关键字

  1. wait 需要搭配 synchronized 关键字使用。
  2. notify 唤醒 wait 不会出现任何异常。

sleep 关键字

  • interrupted 唤醒 sleep 会出现异常。(表示逻辑出现问题)