系统的稳定运行,在单线程程序中得益于类与类之间的协作,在多线程程序中,还得益于线程与线程之间的协作。
一段逻辑代码块的执行可能会依赖于某个先决条件,在单线程程序中可以使用if来构建分支,在多线程程序中可以使用Java提供的等待-通知功能。
例如:生产者消费者模式中,消费者工作的先决条件是产品的数量大于0,生产者工作的先决条件是产品没有造成堆积。
等待:线程因执行目标动作的先决条件暂时没有满足而被暂停的过程。
通知:线程修改了先决条件的结果,使得其他线程可以继续工作而被唤醒的过程。
在Java中,Object类是所有类的父类,Object类提供了实现等待-通知功能的方法,意味着所有对象都具有等待-通知功能,方法如下:
调用wait()会释放当前线程持有的锁并阻塞,直到被其他线程唤醒或等待超时。
notify()会随机唤醒一个线程,notifyAll()会唤醒所有线程。
不管是wait()还是notify(),既然是方法就意味着可以被多个线程反复执行,因此一个对象可能存在多个等待线程。
JVM除了会为每个对象维护一个入口集(Entry Set),用于存储申请对象监视器锁的线程外,还会维护一个等待集(Wait Set)的队列,用于存储该对象上的等待线程。
Object.wait()会释放当前线程抢占的对象锁,然后将当前线程暂停并加入到对象的等待集中。
Object.notify()会唤醒对象等待集中的任意一个线程,被唤醒的线程并不会立马从等待集中剔除,而是继续抢占对象锁,只有当线程成功抢到锁后,才会从等待集中剔除。
wait、notify、notifyAll调用的先决条件是线程已经获得对象锁,因此只能在临界区中调用。
过早唤醒
一个类可能存在多个先决条件,有的线程满足先决条件A时执行,有的线程满足先决条件B时执行。使用notify()可能发生漏唤醒,这时不得不使用notifyAll(),但是notifyAll()会唤醒所有等待线程,使得不该唤醒的线程被唤醒,线程唤醒后抢占不到锁又会被阻塞,会导致过多的线程上下文切换,影响性能。
信号丢失
如果线程在进入wait()前没有判断先决条件是否成立,就会导致其他线程已经修改先决条件结果并发出通知,但是此时等待线程还没有被暂停,也就没法被唤醒,错过了通知信号。将先决条件的判断和wait()放在循环语句中可以解决。
虚假唤醒
等待线程存在没有任何线程通知的情况下被唤醒的可能,虽然概率非常低,但是OS和Java是允许这种情况存在的,如果等待线程被虚假唤醒,但是先决条件却没有成立就会导致问题。将先决条件的判断和wait()放在循环语句中可以解决。
线程调度开销
notify()本身并不会释放锁,只有当同步代码块执行完毕才会释放,这会导致等待线程虽然被唤醒,但是由于抢占不到对象锁而被再次挂起,无故增加了操作系统线程上下文切换的开销。
notify()只会唤醒一个线程,存在漏唤醒信号丢失的可能,notifyAll()效率不高,会唤醒本不应该被唤醒的线程,但是它在正确性方面有保障。
如果满足以下条件,那么优先使用notify(),否则使用notifyAll():
如下例子,分别启动5个生产者和消费者,库存最多为1,生产完即通知消费,消费完即通知生产。
如果不把先决条件和wait()放在循环里,将导致产品被重复消费和生产。
public class Store {
int stock = 0;
synchronized void consumer() {
if (stock <= 0) {
try {
wait();
// 先决条件和wait()应该放在循环中,因为被唤醒后stock可能已经被其他线程消费掉了。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
stock--;
System.out.println(Thread.currentThread().getName() + " 消费成功,库存:" + stock);
notifyAll();
}
synchronized void provider() {
if (stock > 0) {
try {
wait();
// 先决条件和wait()应该放在循环中,因为被唤醒后stock可能已经被其他线程生产,导致重复生产。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
stock++;
notifyAll();
}
public static void main(String[] args) {
Store store = new Store();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
while (true) {
store.consumer();
}
}).start();
}
for (int i = 0; i < 5; i++) {
new Thread(() -> {
while (true) {
store.provider();
}
}).start();
}
}
}
程序运行错误结果:
Thread-0 消费成功,库存:6
Thread-0 消费成功,库存:5
Thread-4 消费成功,库存:4
Thread-4 消费成功,库存:3
Thread-4 消费成功,库存:2
Thread-4 消费成功,库存:1
Thread-4 消费成功,库存:0
Thread-2 消费成功,库存:-1
Thread-1 消费成功,库存:-2
Thread-2 消费成功,库存:-3
Thread-4 消费成功,库存:-4
将if判断改为while循环即可解决该问题。
除了使用Object提供的通知-等待功能外,JUC提供了功能更加强大的条件变量类Condition。
Condition需要配合显示锁Lock使用,增强功能如下:
使用lock.newCondition()即可创建一个Condition实例,每个Condition实例内部都维护了一个存储等待线程的队列,调用不同Condition实例的signal()只会唤醒该队列里的线程,其他队列中的线程不会受影响,解决了notifyAll()线程过早唤醒的问题。
Object.wait()虽然支持等待超时,但是程序无法判断线程被唤醒是因为超时唤醒还是被通知唤醒,Condition支持这种判断。
awaitUntil(Date deadline)
返回一个boolean值,false表示等待超时,true表示被通知唤醒。
如下示例代码,构建了两个Condition实例,不用的业务逻辑执行取决于不同的先决条件:
public class ConditionDemo {
private Lock lock = new ReentrantLock();
//先决条件A
private Condition conditionA = lock.newCondition();
//先决条件B
private Condition conditionB = lock.newCondition();
//逻辑A
void logicA(){
lock.lock();
try {
conditionA.await();
System.out.println(Thread.currentThread().getName() + " logicA...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//逻辑B
void logicB(){
lock.lock();
try {
conditionB.await();
System.out.println(Thread.currentThread().getName() + " logicB...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//只唤醒队列A
void signalA(){
lock.lock();
try {
conditionA.signalAll();
}finally {
lock.unlock();
}
}
//只唤醒队列B
void signalB(){
lock.lock();
try {
conditionB.signalAll();
}finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ConditionDemo demo = new ConditionDemo();
for (int i = 0; i < 5; i++) {
new Thread(()->{
demo.logicA();
}).start();
}
for (int i = 0; i < 5; i++) {
new Thread(()->{
demo.logicB();
}).start();
}
Thread.sleep(1000);
demo.signalA();
Thread.sleep(3000);
demo.signalB();
}
}