线程的等待与通知
- 在Java中,wait() 是 Object 类的一个方法,当线程调用了某个对象的 wait()方法后,它会暂停执行,让当前线程进入等待状态,并释放对象的锁。
- 当前进程从CPU上下来,使用当前线程的程序计数器记录当前的执行情况并把相应数据存到当前栈中,直到其他线程调用相同对象的 notify() 或 notifyAll()方法来唤醒等待的线程。
wait() 方法有几种不同的重载方式:
-
wait():
当前线程进入等待状态,直到其他线程调用相同对象的 notify() 或 notifyAll() 来唤醒。 -
wait(long timeout):
当前线程进入等待状态,最多等待指定的时间(以毫秒为单位),如果在等待时间内没有被唤醒,则自动恢复执行。 -
wait(long timeout, int nanos):
当前线程进入等待状态,最多等待指定的时间(以毫秒和纳秒为单位),如果在等待时间内没有被唤醒,则自动恢复执行。在使用 wait() 方法时需要注意以下几点:
调用
wait()
方法必须在同步代码块或同步方法中,也就是事先获取该对象的监视器锁也就是共享变量的锁,否则会抛出IllegalMonitorStateException
异常。-
同步代码块是通过 synchronized 关键字来实现的一段代码片段,可以使用任意对象作为锁。例如:
synchronized (obj) {//obj为共享变量 // 同步代码块 // 在这里调用 wait() 或 notify() 方法 }
-
同步方法是使用 synchronized 修饰的方法,它的锁对象是该方法所属对象(对于静态同步方法来说是类的 Class 对象)。例如:
public synchronized void method() { // 通过 this 进行同步,相当于 synchronized(this) // 在这里调用 wait() 或 notify() 方法 } public static synchronized void staticMethod() { // 通过类的 Class 对象进行同步,相当于 synchronized(ClassName.class) // 在这里调用 wait() 或 notify() 方法 }
-
唤醒
- 使用 wait() 方法时,应确保在合适的条件下调用,可以使用循环和条件判断来避免虚假唤醒(spurious wakeups)。
- 虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用wait()方法进行防范。退出循环的条件是满足了唤醒该线程的条件。
synchronized (obj) {
while (条件不满足){
obj.wait();
}
}
wait()
方法可以被其他线程通过调用相同对象的notify()
或notifyAll()
来唤醒。当线程被唤醒后,它需要竞争锁才能继续执行。因此,在唤醒之后,需要重新检查等待条件是否满足。- wait()方法常用于多线程间的协调和同步操作,允许线程之间进行有效地通信和资源共享。
生产者和消费者举例
public class BreadShop {
private static final int MAX_QUEUE_SIZE = 10; // 队列最大容量为10
private LinkedList<String> queue = new LinkedList<>(); // 创建一个空队列
// 生产者线程,模拟顾客购买面包,将面包加入队列
public void produceBread(String bread) throws InterruptedException {
synchronized (queue) {
while (queue.size() >= MAX_QUEUE_SIZE) { // 当队列已满时
System.out.println("队列已满,无法继续生产面包,等待消费者购买...");
queue.wait(); // 暂停当前线程的执行,并释放锁
}
//存到栈中记录执行情况,等待被唤醒继续执行
queue.add(bread); // 将面包添加到队列中
System.out.println("顾客购买了面包:" + bread);
System.out.println("当前队列长度:" + queue.size());
queue.notifyAll(); // 唤醒正在等待的消费者线程
}
}
// 消费者线程,模拟顾客购买面包,从队列中取出面包消费
public void consumeBread() throws InterruptedException {
synchronized (queue) {
while (queue.isEmpty()) { // 当队列为空时
System.out.println("队列为空,无面包可供消费,等待生产者生产...");
queue.wait(); // 暂停当前线程的执行,并释放锁
}
String bread = queue.poll(); // 从队列中取出面包
System.out.println("顾客购买并消费了面包:" + bread);
System.out.println("当前队列长度:" + queue.size());
queue.notifyAll(); // 唤醒正在等待的生产者线程
}
}
public static void main(String[] args) {
BreadShop breadShop = new BreadShop();
Thread producerThread = new Thread(() -> {
try {
while (true) {
String bread = "面包" + System.currentTimeMillis();
breadShop.produceBread(bread); // 生产面包并加入队列
Thread.sleep(1000); // 生产一个面包需要一定时间
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumerThread = new Thread(() -> {
try {
while (true) {
breadShop.consumeBread(); // 从队列中取出面包并消费
Thread.sleep(2000); // 消费一个面包需要一定时间
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producerThread.start();
consumerThread.start();
}
}
在上述代码中,当队列已满时,生产者线程会执行queue.wait()暂停当前线程的执行,并释放锁。这样做的目的是等待消费者线程购买面包后将队列腾出空间。一旦队列不再满,也就是有消费者购买了面包并释放了队列空间,生产者线程会被唤醒,然后继续执行queue.add(bread)将面包添加到队列中。注意,这个唤醒的操作是由消费者线程在购买面包后执行queue.notifyAll()来触发的,来唤醒所有等待该对象锁的线程。它们会重新竞争获取对象锁并继续执行消费的操作。因此,在生产者消费者模型中使用notifyAll()方法时,唤醒的是所有等待该对象锁的线程。具体是生产者线程还是消费者线程取决于谁先获得对象锁并开始执行。通过这种方式,生产者和消费者线程可以实现同步,确保在队列满时生产者暂停生产,直至有空间可供添加新的面包。
notifyAll()
方法会唤醒正在等待当前对象锁的所有线程,也就是说会唤醒所有对同一变量进行锁的所有对象,例如:对queue进行监视的生产者和消费者,并且它们会竞争获取该对象的锁。
注意
- 即使只有消费者线程在等待获取对象锁,也不会出现误唤醒的情况,因为被唤醒的线程需要再次竞争获取锁才能继续执行。生产者和消费者线程一起竞争锁,并且只有一个线程能够成功获取锁并执行。
- 在如上代码中假如生产者线程A首先通过synchronized获取到了queue上的锁,那么后续所有企图生产元素的线程和消费线程将会在获取该监视器锁的地方被阻塞挂起。线程A获取锁后发现当前队列已满会调用queue.wait()方法阻塞自己,然后释放获取的queue上的锁,这里考虑下为何要释放该锁?如果不释放,由于其他生产者线程和所有消费者线程都已经被阻塞挂起,而线程A也被挂起,这就处于了死锁状态。这里线程A挂起自己后释放共享变量上的锁,就是为了打破死锁必要条件之一的持有并等待原则。
// 创建资源
private static volatile Object resourceA = new Object();
private static volatile Object resourceB = new Object();
public static void main(String[] args) throws InterruptedException {
// 创建线程
Thread threadA = new Thread(new Runnable() {
public void run() {
try {
// 获取resourceA共享资源的监视器锁
synchronized (resourceA) {
System.out.println("threadA get resourceA lock");
// 获取resourceB共享资源的监视器锁
synchronized (resourceB) {
System.out.println("threadA get resourceB lock");
// 线程A阻塞,并释放获取到的resourceA的锁
System.out.println("threadA release resourceA lock");
resourceA.wait();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 创建线程
Thread threadB = new Thread(new Runnable() {
public void run() {
try {
//休眠1s
Thread.sleep(1000);
// 获取resourceA共享资源的监视器锁
synchronized (resourceA) {
System.out.println("threadB get resourceA lock");
System.out.println("threadB try get resourceB lock...");
// 获取resourceB共享资源的监视器锁
synchronized (resourceB) {
System.out.println("threadB get resourceB lock");
// 线程B阻塞,并释放获取到的resourceA的锁
System.out.println("threadB release resourceA lock");
resourceA.wait();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动线程
threadA.start();
threadB.start();
// 等待两个线程结束
threadA.join();
threadB.join();
System.out.println("main over");
}
执行结果:
在main函数里面启动了线程A和线程B,为了让线程A先获取到锁,这里让线程B先休眠了1s,线程A先后获取到共享变量resourceA和共享变量resourceB上的锁,然后调用了resourceA的wait()方法阻塞自己,阻塞自己后线程A释放掉获取的resourceA上的锁。线程B休眠结束后会首先尝试获取resourceA上的锁,如果当时线程A还没有调用wait()方法释放该锁,那么线程B会被阻塞,当线程A释放了resourceA上的锁后,线程B就会获取到resourceA上的锁,然后尝试获取resourceB上的锁。由于线程A调用的是resourceA上的wait()方法,所以线程A挂起自己后并没有释放获取到的resourceB上的锁,所以线程B尝试获取resourceB上的锁时会被阻塞。
- 这就证明了当线程调用共享对象的wait()方法时,当前线程只会释放当前共享对象的锁,当前线程持有的其他共享对象的监视器锁并不会被释放。