目录
一、volatile 关键字
1、volatile 能保证内存可见性
2、volatile 不保证原子性
二、wait 和 notify
1、wait()方法
2、notify()方法
3、notifyAll()方法
4、wait 和 sleep 的对比
一、volatile 关键字
1、volatile 能保证内存可见性
我们前面的线程安全文章中,分析引起线程不安全的原因,其中就有一个原因是可见性,若一个线程对一个共享变量的修改,不能让其他线程看到,则会引起线程安全问题。因此,我们就引入了volatile 关键字,volatile 修饰的变量,能够保证 "内存可见性"。
(这里的“工作内存”不是真正的内存,就像CPU寄存器。)
代码在写入 volatile 修饰的变量的时候:
- 改变线程 工作内存 中volatile变量副本的值
- 将改变后的副本的值从 工作内存 刷新到 主内存
代码在读取 volatile 修饰的变量的时候:
- 从主内存中读取volatile变量的最新值到线程的工作内存中
- 从工作内存中读取volatile变量的副本
我们在讨论内存可见性时说, 直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存),速度非 常快,但是可能出现数据不⼀致的情况。 加上 volatile,就强制读写内存,速度是慢了, 但是数据变的更准确了。
代码示例:
在这个代码中
- 创建两个线程 t1 和 t2
- t1 中包含⼀个循环, 这个循环以 flag == 0 为循环条件.
- t2 中从键盘读入⼀个整数, 并把这个整数赋值给 flag
- 预期当用户输入非 0 的值的时候, t1 线程结束.
static class Counter {public int flag = 0;
}
public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(() -> {while (counter.flag == 0) {// do nothing}System.out.println("循环结束!");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("输⼊⼀个整数:");counter.flag = scanner.nextInt();});t1.start();t2.start();
}
// 执⾏效果
// 当用户输⼊⾮0值时, t1 线程循环不会结束. (这显然是⼀个 bug)
这里t1线程循环并不会结束,这是因为 t1 读的是自己工作内存中的内容,当 t2 对 flag 变量进行修改,此时 t1 感知不到 flag 的变化。
如果给 flag 加上 volatile:
static class Counter {public volatile int flag = 0;
}
// 执⾏效果
// 当用户输⼊⾮0值时, t1 线程循环能够⽴即结束.
2、volatile 不保证原子性
volatile 和 synchronized 有着本质的区别,synchronized 能够保证原子性,volatile 保证的是内存可见 性。
这里可以用我们前面线程安全那一篇文章中的代码来证明,将 synchronized去掉,加上对count变量的 volatile 修饰。
public class ThreadDemo {private static volatile long count = 0;public static void main(String[] args) throws InterruptedException{Thread t1 = new Thread(()->{for (int i = 1;i <= 500000;i++) {count++;}});Thread t2 = new Thread(()->{for (long i = 0;i < 500000;i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println("count= "+count);}
}
可见运行结果为:
此时,最终 count 的值仍然无法保证是 1000000。所以volatile 不保证原子性,volatile 保证的是内存可见性。
二、wait 和 notify
由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知,但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序。
就比如打球,球场上的每个运动员都是独立的 "执行流" ,可以认为是⼀个 "线程"。而完成⼀个具体的进攻得分动作,则需要多个运动员相互配合,按照⼀定的顺序执行⼀定的动作,线程 1 先 "传球",线程 2 才能 "扣篮"。
完成这个协调工作,主要涉及到三个方法:
- wait() / wait(long timeout):让当前线程进入等待状态
- notify() / notifyAll():唤醒在当前对象上等待的线程
注意:wait, notify, notifyAll 都是 Object 类的方法
1、wait()方法
wait 做的事情:
- 使当前执行代码的线程进行等待 (把线程放到等待队列中)
- 释放当前的锁
- 满足一定条件时被唤醒,重新尝试获取这个锁.
这里要注意,wait 要搭配 synchronized 来使用,脱离 synchronized 使用 wait 会直接抛出异常。
wait 结束等待的条件:
- 其他线程调用该对象的 notify 方法
- wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本,来指定等待时间)
- 其他线程调用该等待线程的 interrupted 方法,导致 wait 抛出 InterruptedException 异常
代码示例:观察wait()方法使用
public static void main(String[] args) throws InterruptedException {Object object = new Object();synchronized (object) {System.out.println("等待中");object.wait();System.out.println("等待结束");}
}
这样在执行到 object.wait() 之后就⼀直等待下去,当然程序肯定不能一直这么等待下去了,这个时候就 需要使用到另外⼀个方法,唤醒的方法notify()。
2、notify()方法
notify 方法是唤醒等待的线程的:
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
- 如果有多个线程等待,则由线程调度器随机挑选出⼀个呈 wait 状态的线程。(并没有 "先来后到")
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁
代码示例:使用notify()方法唤醒线程
- 创建 WaitTask 类, 对应⼀个线程, run 内部循环调用wait.
- 创建 NotifyTask 类, 对应另⼀个线程, 在 run 内部调用一次 notify
- 注意, WaitTask 和 NotifyTask 内部持有同⼀个 Object locker,WaitTask 和 NotifyTask 要想配合就需要搭配同一个 Object。
public class ThreadDemo {static class WaitTask implements Runnable {private Object locker;public WaitTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {while (true) {try {System.out.println("wait 开始");locker.wait();System.out.println("wait 结束");} catch (InterruptedException e) {e.printStackTrace();}}}}}static class NotifyTask implements Runnable {private Object locker;public NotifyTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {System.out.println("notify 开始");locker.notify();System.out.println("notify 结束");}}}public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(new WaitTask(locker));Thread t2 = new Thread(new NotifyTask(locker));t1.start();Thread.sleep(1000);t2.start();}
}
3、notifyAll()方法
notify方法只是唤醒某一个等待线程,使用notifyAll方法可以一次唤醒所有的等待线程。
范例:使用notifyAll()方法唤醒所有等待线程,在上面的代码基础上做出修改
- 创建 3 个 WaitTask 实例,1 个 NotifyTask 实例.
static class WaitTask implements Runnable {// 代码不变
}
static class NotifyTask implements Runnable {// 代码不变
}public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(new WaitTask(locker));Thread t3 = new Thread(new WaitTask(locker));Thread t4 = new Thread(new WaitTask(locker));Thread t2 = new Thread(new NotifyTask(locker));t1.start();t3.start();t4.start();Thread.sleep(1000);t2.start();}
此时可以看到,调用notify 只能唤醒⼀个线程 。
- 修改 NotifyTask 中的 run 方法,把 notify 替换成 notifyAll
public void run() {synchronized (locker) {System.out.println("notify 开始");locker.notifyAll();System.out.println("notify 结束");}}
此时可以看到,调用 notifyAll 能同时唤醒 3 个wait 中的线程。
注意:虽然是同时唤醒 3 个线程,但是这 3 个线程需要竞争锁,所以并不是同时执行,而仍然是有先有后的执行。
理解 notify 和 notifyAll:notify 只唤醒等待队列中的一个线程,其他线程还是乖乖等着notifyAll ⼀下全都唤醒,需要这些线程重新竞争锁
4、wait 和 sleep 的对比
其实理论上 wait 和 sleep 完全是没有可比性的,因为⼀个是用于线程之间的通信的,⼀个是让线程阻塞⼀段时间。
唯⼀的相同点就是都可以让线程放弃执行⼀段时间。
但我们还是要总结下:
- wait 需要搭配 synchronized 使用,sleep 不需要。
- wait 是 Object 的方法,sleep 是 Thread 的静态方法。