wait()、notify()、notifyAll()、都是Object中的final native方法,用于线程通信,是Java通过消息传递来实现线程通信的方式。他们都只能在同步方法或同步代码块中执行,而且必须是内置锁。
wait()
语义:使当前线程立即停止运行,释放对象锁,处于等待阻塞状态,并将当前线程置入锁对象的等待池中,直到被唤醒或被中断为止。
重载:
- wait();死等,直到被唤醒或被中断。
- wait(long timeout);超时等待:若在规定的时间内未被唤醒,则继续执行
- wait(long timeout,int nanos);在上一个方法的基础上增加了纳秒控制。
notify()
语义:用来唤醒一个随机等待该对象锁的其他线程,notify方法被调用后,当前线程不会立即释放对象锁,要等到当前线程执行完毕后再释放锁。
notifyAll()
语义:用来唤醒等待该对象的所有线程。
现在我们来看看下面的程序
public class Demo {
/**
* 锁对象
*/
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
MyThread1 myThread1 = new MyThread1();
myThread1.start();
System.out.println("子线程启动");
synchronized (lock) {
System.out.println("主线程获得同步锁“);
System.out.println("主线程释放了同步锁,阻塞");
lock.wait();
}
System.out.println("主线程执行结束");
}
static class MyThread1 extends Thread {
@Override
public void run() {
synchronized (lock) {
System.out.println("子线程获得同步锁");
}
System.out.println("子线程释放了同步锁,子线程执行结束");
}
}
}
以上代码在主线程和子线程中都有同步代码块,而且两个同步代码块都使用相同的对象同步锁。
由于CPU调度两个同步代码块的执行顺序可能先后不一定:
- 主线程先抢到lock对象锁:随后执行lock.wait(),主线程处于等待状态,释放了同步锁。子线程立即获得了同步锁,执行完线程体。而主线程依旧处于等待阻塞状态。
子线程启动
主线程获得同步锁
主线程释放了同步锁,阻塞
子线程获得同步锁
子线程释放了同步锁,子线程执行结束
- 子线程先抢到lock对象锁:随后执行完线程体,释放同步锁。主线程立即获得了同步锁,执行lock.wait(),主线程处于等待状态,释放了同步锁。
子线程启动
子线程获得同步锁
子线程释放了同步锁,子线程执行结束
主线程获得同步锁
主线程释放了同步锁,阻塞
以上两种执行过程,主线程最终都处于等待阻塞状态,因为释放了同步锁后没有再次被唤醒。
如果给wait()方法传入等待时长,则两种执行过程都会在时长结束后正常结束。
如果在子线程体中使用notify唤醒:
static class MyThread1 extends Thread {
@Override
public void run() {
synchronized (lock) {
System.out.println("子线程获得同步锁");
System.out.println("唤醒其他线程");
lock.notify();
}
System.out.println("子线程释放了同步锁,子线程执行结束");
}
}
我们再来分析执行过程,依旧有两种:
- 主线程先抢到lock对象锁:随后执行lock.wait(),主线程处于等待状态,释放了同步锁。子线程立即获得了同步锁,执行lock.notify(),唤醒其他线程,但此时还未释放同步锁,等到执行完线程体,同步锁才被释放。主线程立即获取到同步锁,进而继续执行lock.wait()之后的语句,主线程正常执行完毕。
子线程启动
主线程获得同步锁
主线程释放了同步锁,阻塞
子线程获得同步锁
唤醒其他线程
子线程释放了同步锁,子线程执行结束
主线程执行结束
- 子线程先抢到lock对象锁:随后执行lock.notify()(此时并没有线程处于等待状态),执行完线程体,释放同步锁。主线程立即获得了同步锁,执行lock.wait(),主线程处于等待状态,释放了同步锁,但主线程再也不会被唤醒了。
子线程启动
子线程获得同步锁
唤醒其他线程
子线程释放了同步锁,子线程执行结束
主线程获得同步锁
主线程释放了同步锁,阻塞
所以,程序按方式1执行则能够正常结束。
锁和监视器
上述程序同步代码块的同步锁对象是lock,因此都是通过lock.wait()和ock.notify()方法来实现线程通信,如果直接书写成wait()和notify(),会报非法的监视器状态异常
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
at java.base/java.lang.Object.notify(Native Method)
at com.jc.thread.Demo$MyThread1.run(Demo.java:26)
所以lock拥有一个同步锁和一个监视器。
在Java中,每个对象和Class内部都有一个锁,都会和一个监视器关联。锁是存在于对象内部的数据结构,监视器是操作系统层次的概念,是一个独立的结构但是和对象关联。
锁池和等待池
- 锁池:多个线程共同争夺同一临界资源,而线程访问临界资源必须先持有临界资源的同步锁,当A线程持有了同步锁,那么其他线程就只能进入锁对象的锁池中,处于同步阻塞状态。等待同步锁被释放。然后被CPU调度,才能继续访问该临界资源。
- 等待池:当A线程访问了临界资源,但调用了锁对象的wait()方法,线程A就释放了该同步锁,进入了锁对象的等待池中,处于等待阻塞状态。等待被持有该同步锁的其他线程唤醒执行notify()/notifyAll(),才能进入锁池,等待重新被调度。
notify 和 notifyAll
两者都是只能被持有同步锁的线程调用,
- notify只能从等待池中随机唤醒一个等待线程进入锁池。
- notifyAll是唤醒所有的等待线程到锁池中。
在锁池中的线程共同参与同步锁的竞争,能否获取到则取决于线程的优先级及CPU的调度。只有线程再次调用wait()方法才会又进入等待池。
请看下面的代码
public class Demo {
/**
* 锁对象
*/
public static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
new MyTHread().start();
new MyTHread().start();
// 确保子线程都处于等待状态
Thread.sleep(1000);
synchronized (lock) {
System.out.println("主线程获取同步锁");
lock.notify();
}
System.out.println("主线程执行完毕");
}
static class MyTHread extends Thread {
@Override
public void run() {
synchronized (lock) {
try {
System.out.println(Thread.currentThread().getName() + " 获取同步锁,等待……");
lock.wait();
System.out.println(Thread.currentThread().getName() + " 被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " 释放同步锁");
}
}
}
先让三个子线程处于等待阻塞状态,然后主线程获取同步锁,执行notify操作。运行结果如下:
Thread-0 获取同步锁,等待
Thread-1 获取同步锁,等待
主线程获取同步锁
Thread-0 被唤醒
Thread-0 释放同步锁
如上,0号线程被主线程唤醒,执行完同步代码块。而1号线程则会一直处于等待阻塞状态,这样就造成了死锁。
我们来看下死锁的定义:
- 死锁是指两个或两个以上的进程在执行过程中,由于
竞争资源
或者由于彼此通信而造成的一种阻塞
的现象,若无外力作用,它们都将无法推进下去。
这里所说的外力指的就是notify/notifyAll操作,那么notify造成的死锁就是由于线程通信造成的阻塞状态
如果使用notifyAll操作,运行结果如下:
Thread-0 获取同步锁,等待……
Thread-1 获取同步锁,等待……
主线程获取同步锁
Thread-0 被唤醒
Thread-1 被唤醒
Thread-0 释放同步锁
Thread-1 释放同步锁
主线程执行完毕
notifyAll从等待池中唤醒了所有的线程到锁池中,那么都能被CPU调度从而正常执行完程序。
正确地使用 wait、notify、notifyAll
- 通过同步锁对象调用
- 只能在synchronized中调用
- 永远在循环里调用,而不要使用if
- notify可能会造成死锁,更多的使用notifyAll
对于上面第三点,你的线程可能被错误唤醒,将会继续执行后序的代码,从而造成数据被破坏。如在生产者消费者的例子中,缓冲区为满的时候生产者线程被唤醒,如果wait在if中被调用,则不会再次检查缓冲区,将继续生成数据。
生产者-消费者模式
/**
* 资源类
*/
public class Source {
/**
* 资源数据
*/
private int data;
/**
* 最大资源数
*/
private final int MAX = 10;
/**
* 添加资源
*/
public void push() throws InterruptedException {
synchronized (this) {
// 判断资源数是否达上线
while (data == MAX) {
System.out.println("添加失败,资源已满");
wait();
}
// 生产资源
data++;
// 唤醒其他线程操作资源
notifyAll();
System.out.println("添加资源成功,data=" + data);
}
}
/**
* 取出资源
*/
public void pop() throws InterruptedException {
synchronized (this) {
// 判断资源是否被消耗完
while (data == 0) {
System.out.println("取出失败,没有资源");
wait();
}
// 消费资源
data--;
// 唤醒其他线程操作资源
notifyAll();
System.out.println("取出资源成功,data=" + data);
}
}
}
/**
* 消费者
*/
public class Customer extends Thread{
private Source source;
public Customer(Source source) {
this.source = source;
}
@Override
public void run() {
while(true){
try {
sleep(50);
source.pop();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 生产者
*/
public class Productor extends Thread {
private Source source;
public Productor(Source source) {
this.source = source;
}
@Override
public void run() {
while (true){
try {
sleep(70);
source.push();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 生产者-消费者
*/
public class Demo {
public static void main(String[] args) {
Source source = new Source();
new Productor(source).start();
new Customer(source).start();
new Productor(source).start();
new Productor(source).start();
new Customer(source).start();
}
}
执行结果:
取出失败,没有资源
取出失败,没有资源
添加资源成功,data=1
取出资源成功,data=0
添加资源成功,data=1
添加资源成功,data=2
取出资源成功,data=1
取出资源成功,data=0
取出失败,没有资源
添加资源成功,data=1
取出资源成功,data=0
添加资源成功,data=1
添加资源成功,data=2
取出资源成功,data=1
取出资源成功,data=0
添加资源成功,data=1
添加资源成功,data=2
......