文章目录
- 一、volatile关键字
- 二、wait 和 notify
- wait
- notify
- notifyAll
- wait 和 sleep 的区别
- 顺序打印ABC
一、volatile关键字
volatile关键字的存在是用来解决内存可见性问题的。
我在 :线程安全问题 这篇文章中介绍过内存可见性问题。
前面我们讨论内存可见性时说了, 直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度
非常快, 但是可能出现数据不一致的情况.
public class Test {static int flg = 0;public static void main(String[] args) {Scanner scan = new Scanner(System.in);Thread t1 = new Thread(() -> {while(flg == 0) {}System.out.println("t1循环结束");});Thread t2 = new Thread(() -> {System.out.println("输入一个整数");flg = scan.nextInt();});t1.start();t2.start();}
}
当我们输入1时,t1线程并没有循环结束,我们可以打开jconsle,观看一下线程信息
我们可以发现此时t2线程已经执行完成了,但我们的t1仍然在执行。
我们可以分析一下t1线程所进行的操作,flg == 0需要进行两步操作:
- load 把主内存的flg值读取到工作内存
- cmp 将工作内存的值与0相比较,决定程序执行。
我们t1线程在t2线程修改flg值之前,load的结果都是一样的,而且load 的 速度与cmp操作相比,速度慢了很多,于是我们JVM就会大胆的认为没人改flg值了,于是不再重复load了。
正是因为这样的误判存在,所以volatile关键字的作用就来了,我们可以给变量flg加上volatile关键字,就是告诉编译器,每次都得load这个变量,可能会发生改变。
我们可以发现,当我们加上volatile关键字之后,程序达到了我们预期的效果。
我们上述所看到的内存可见性 编译器优化的问题 是有发生的可能,但也不是百分百出现的。
我们在循环的时候,代码稍加改写,加入sleep,让循环执行慢一点,也达到了预期的效果,但这也存在不确定性,最稳妥的做法就是对这些可能存在内存不可见问题的变量加上volatile关键字,volatile保证数据准确的同时,执行速度也会有所下降。
volatile不能保证原子性,原子性靠synchronized保证
二、wait 和 notify
由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知. 但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.
我们之前学习的join,sleep也能达到部分的目的,但是join,sleep太过于死板,不够灵活,join必须等t1执行完毕,t2才能执行。sleep必须得指定一个时间,但线程执行的时间我们人为不好估计。
wait 和 notify 可以很好的解决上述问题。
注意: wait, notify, notifyAll 都是 Object 类的方法.
wait
wait使该线程进行阻塞等待
一样的,我们在使用wait的使用需要抛一个中断异常。
public static void main(String[] args) throws InterruptedException {Object o = new Object();o.wait();}
我们可以发现报了一个非法锁异常,要想理解这个异常我们需要明白wait所做的事情都是什么:
- 释放锁
- 进行阻塞等待(等待队列)
- 满足条件后,重新尝试获取锁。
现在我们就可以理解这个异常了,我们都没锁,何谈释放锁,就好比我们是单身状态,何谈分手之言。
所以,wait 需要和 synchronized搭配使用的
public static void main(String[] args) throws InterruptedException {Object o = new Object();synchronized (o) {System.out.println("wait前");o.wait();System.out.println("wait后");}}
怎么唤醒呢?下面我们来了解一下notify关键字
notify
notify 方法用来唤醒wait等待的线程.
该方法同样的,必须在synchronized(obj){}代码块内部使用
public static void main(String[] args) {Object o = new Object();Thread t1 = new Thread(() -> {System.out.println("wait前");try {synchronized (o) {o.wait();}} catch (InterruptedException e) {throw new RuntimeException(e);}});Thread t2 = new Thread(() -> {System.out.println("notify之前");synchronized (o) {o.notify();}System.out.println("notify之后");});t1.start();t2.start();}
我们使用notify的对象和使用wait的对象必须相同,因为notify只能唤醒同一对象上等待的线程。
为什么我们这里的结果和我们的预期有些差异
当我们启动t1,t2线程时,由于线程调度是随机的,不能够保证t1,t2的执行顺序,如果先执行了t2的notify,后执行的t1的wait,此时notify不会起任何意义的,但也对程序没什么坏的影响。
所以我们在使用wait 和 notify一定要保证他们的执行顺序。
我们可以加一个sleep,让t1线程的wait启动后,再去执行t2现在的notify,当然这个sleep的时间不确定,具体取决于线程调度的时间。
但要是我们仍害怕notify先执行了,wait就只能死等下去了,那么我们可以wait带参数版本
方法 | 作用 |
---|---|
wait() | 死等 |
wait(long miles) | 执行等待最大时间 |
注意:
1.如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
2.在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
notifyAll
notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.
notify就相当于我们宿舍的卫生间,自己出来之后,随机告诉一个舍友你现在可以去了。
notify 只唤醒等待队列中的一个线程. 其他线程还是处于wait状态
notifyAll相当于,同时向宿舍的所有舍友喊我出来了,你们可以进去了。
notifyAll 一下全都唤醒, 需要这些线程重新竞争锁
wait 和 sleep 的区别
相同点: sleep()和wait()都可以暂停线程的执行。
不同点:
1.wait是Object的方法,sleep是Thread的静态方法
2. wait 需要搭配 synchronized 使用. sleep 不需要.
3. sleep()方法睡眠指定时间之后,线程会自动苏醒。wait()方法被调用后,可以通过notify()或notifyAll()来唤醒wait的线程。
4. notify唤醒wait是正常的,interrput中断sleep是异常的
5. sleep()常用于一定时间内暂停线程执行,wait()常用于线程间交互和通信。
顺序打印ABC
有三个线程,分别只能打印ABC,三个线程按照ABC的顺序打印。
public static void main(String[] args) throws InterruptedException {Object lock1 = new Object();Object lock2 = new Object();Thread t1 = new Thread(() -> {System.out.println("A");synchronized (lock1) {lock1.notify();}});Thread t2 = new Thread(() -> {try {synchronized (lock1) {lock1.wait();}} catch (InterruptedException e) {e.printStackTrace();}System.out.println("B");synchronized (lock2) {lock2.notify();}});Thread t3 = new Thread(() -> {try {synchronized (lock2) {lock2.wait();}} catch (InterruptedException e) {e.printStackTrace();}System.out.println("C");});t2.start();t3.start();Thread.sleep(100);t1.start();}
这里线程启动的顺序为什么是这样,因为我们在t1的notify执行之前,必须保证t2,t3的wait启动。