原文:https://wiki.sei.cmu.edu/confluence/display/java/THI02-J.+Notify+all+waiting+threads+rather+than+a+single+thread
当线程调用Object.wait()
方法后,等待被唤醒并在预设条件满足时继续执行,根据THI03-J. Always invoke wait() and await() methods inside a loop的建议,当接收到通知时,等待线程会检验预设条件是否成立,否则必须保持等待状态。
java.lang.Object
提供notify()
和notifyAll()
方法用于唤醒单个或一组线程。调用这两个方法的线程必须和等待线程持有同一个对象锁,否则,就会抛出IllegalMonitorStateException
异常。notifyAll()
会唤醒一组等待这个对象锁的线程,如果它们的预设条件满足,就会恢复执行。而且,如果这一组线程在进入等待状态之前持有另一个特定的对象锁,那么,这组被通知的线程中只有一个能获取这个特定的对象锁,因此,其他的线程仍然会保持等待。notify()
方法会唤醒一个线程,但并不保证会唤醒哪一个。如果被唤醒的线程预设条件不满足,它还是会保持等待。这往往与我们通知唤醒的初衷不符合。
因此,只有在以下条件都满足的条件下,才允许调用notify()
方法:
- 所有等待线程使用相同的预设条件
- 被唤醒后,所有的线程都执行一系列相同的操作。也就是任何一个线程都可以被唤醒继续执行
- 只有一个线程需要被唤醒
对于一组线程,如果它们是一样的执行逻辑,并且提供无状态的服务,那么,就是满足以上条件的。
java.util.concurrent.locks
提供了Condition.signal()
和Condition.signalAll()
方法用于唤醒阻塞在Condition.await()
的调用。
如果使用java.util.concurrent.locks.Lock
,那么久需要用到Condition
对象。尽管Lock
对象也可以使用Object.wait()
,Object.notify()
,Object.notifyAll()
这些方法。但是根据LCK03-J. Do not synchronize on the intrinsic locks of high-level concurrency objects的解释,应该避免调用这些方法。同步代码使用Lock
对象应该关联使用一个或多个Condition
,而不是使用对象自身的锁。Lock
对象直接提供了加锁策略,因此,对于Lock
对象,应该使用await()
,signal()
,signalAll()
这些方法,而不是wait()
,notify()
,notifyAll()
。
只有在以下条件都满足的条件下,才允许调用signal()
方法:
- 所有等待线程的
Condition
对象是一样的
- 被唤醒后,所有的线程都执行一系列相同的操作。也就是任何一个线程都可以被唤醒继续执行
- 只有一个线程需要被唤醒
或者,以下条件都满足:
- 每个线程使用唯一的
Conditon
对象
- 每个
Conditon
对象都关联同一个Lock
对象
如果安全地使用,signal()
方法比signalAll()
有更好的性能。
如果调用notify()
或signal()
唤醒一个线程,但线程预设条件没有满足,那么它会继续等待,结果就是没有一个线程被唤醒继续执行,系统功能可能会停滞。
不规范的代码示例(notify())
这段代码需要在多线程环境下完成一个复杂的多步骤的流程。每个线程执行的步骤是由time
属性决定的,没个线程都在等待自己执行的时机,线程执行完一个步骤,time
加1,并通知后续线程执行。
public final class ProcessStep implements Runnable {
private static final Object lock = new Object();
private static int time = 0;
private final int step; // Do Perform operations when field time
// reaches this value
public ProcessStep(int step) {
this.step = step;
}
@Override public void run() {
try {
synchronized (lock) {
while (time != step) {
lock.wait();
}
// Perform operations
time++;
lock.notify();
}
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // Reset interrupted status
}
}
public static void main(String[] args) {
for (int i = 4; i >= 0; i--) {
new Thread(new ProcessStep(i)).start();
}
}
}
这段逻辑违背了活跃性条件,也就是,每个线程都有不一样的预设条件,但是notify()
方法每次只唤醒一个线程,除非恰好唤醒了下一步应该执行的线程,否则,程序就会死锁。
正确的方案(notifyAll()
)
下面的代码中,每个线程完成自己的步骤后,会调用notifyAll()
唤醒所有的线程。被唤醒的线程如果发现自己的预设条件满足,就可以继续执行,其他线程预设条件不满足,就会保持等待状态。
对比上述不规范代码,只是修改了run()
方法。
public final class ProcessStep implements Runnable {
private static final Object lock = new Object();
private static int time = 0;
private final int step; // Perform operations when field time
// reaches this value
public ProcessStep(int step) {
this.step = step;
}
@Override public void run() {
try {
synchronized (lock) {
while (time != step) {
lock.wait();
}
// Perform operations
time++;
lock.notifyAll(); // Use notifyAll() instead of notify()
}
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // Reset interrupted status
}
}
}
不规范代码示例(Conditon Interface)
类似之前的代码,只是使用了Conditon
的方式通知。
public class ProcessStep implements Runnable {
private static final Lock lock = new ReentrantLock();
private static final Condition condition = lock.newCondition();
private static int time = 0;
private final int step; // Perform operations when field time
// reaches this value
public ProcessStep(int step) {
this.step = step;
}
@Override public void run() {
lock.lock();
try {
while (time != step) {
condition.await();
}
// Perform operations
time++;
condition.signal();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // Reset interrupted status
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
for (int i = 4; i >= 0; i--) {
new Thread(new ProcessStep(i)).start();
}
}
}
类似使用Object.notify()
,signal()
方法可能唤醒任意的线程。
正确的方案(signalAll()
)
使用signalAll()
通知所有等待的线程,在await()
方法返回前,当前线程会获取这个条件关联的锁,当线程退出时,可以确保持有这个锁,预设条件满足的线程可以继续执行,其他线程会保持等待。
public class ProcessStep implements Runnable {
private static final Lock lock = new ReentrantLock();
private static final Condition condition = lock.newCondition();
private static int time = 0;
private final int step; // Perform operations when field time
// reaches this value
public ProcessStep(int step) {
this.step = step;
}
@Override public void run() {
lock.lock();
try {
while (time != step) {
condition.await();
}
// Perform operations
time++;
condition.signalAll();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // Reset interrupted status
} finally {
lock.unlock();
}
}
}
正确的方案(每个线程都有唯一的条件)
为每个线程分配一个自己的等待条件,所有的线程都可以访问所有的条件。
// Declare class as final because its constructor throws an exception
public final class ProcessStep implements Runnable {
private static final Lock lock = new ReentrantLock();
private static int time = 0;
private final int step; // Perform operations when field time
// reaches this value
private static final int MAX_STEPS = 5;
private static final Condition[] conditions = new Condition[MAX_STEPS];
public ProcessStep(int step) {
if (step <= MAX_STEPS) {
this.step = step;
conditions[step] = lock.newCondition();
} else {
throw new IllegalArgumentException("Too many threads");
}
}
@Override public void run() {
lock.lock();
try {
while (time != step) {
conditions[step].await();
}
// Perform operations
time++;
if (step + 1 < conditions.length) {
conditions[step + 1].signal();
}
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // Reset interrupted status
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
for (int i = MAX_STEPS - 1; i >= 0; i--) {
ProcessStep ps = new ProcessStep(i);
new Thread(ps).start();
}
}
虽然使用了signal()
方法,但只有满足这个特定条件的线程才会被唤醒。需要注意,这个方案安全地前提是,不存在不安全的代码使用这个实例创建新的线程。