在多线程之间的同步中,一般采用Object.wait()、Object.notify()、Object.notifyAll()来实现,而notify可能会导致多线程之间出现死锁,下面通过生产者消费者的示例来说明:
生产者代码:
while(true) {
synchronized (queue) {
while (queue.size() == maxSize) { #ref_p1
try {
System.out.println("已满");
queue.wait(); #ref_p2
System.out.println("已唤醒生产者---");
} catch (InterruptedException e) {
System.out.println("生产者报错");
}
}
Random random = new Random();
int i = random.nextInt();
System.out.println("Produce " + i);
queue.add(i);
queue.notify();
}
}
消费者代码:
while(true) {
synchronized (queue) {
while (queue.isEmpty()) {
try {
System.out.println("已空");
queue.wait(); #ref_c1
System.out.println("已唤醒消费者---");
} catch (InterruptedException e) {
System.out.println("消费者报错");
}
}
int v = queue.remove();
System.out.println("Consume " + v);
queue.notify();
}
}
队列queue的最大长度设为1,假设有2个生产者线程P1、P2,2个消费者线程C1、C2,黄色表示线程调用wait阻塞,红色表示线程获取锁阻塞。
1. C1和C2首先执行,由于初始队列为空,故阻塞在#ref_c1处,此时4个线程状态如下:
2. P1获取到锁执行,往队列中插入数据,并调用notify释放锁,假设此时C1被唤醒,P2获取到锁,则P1在下一次循环中获取锁阻塞
3. 假设P2获取到锁开始执行到#ref_p1处时,C1还没有从队列中获取数据,则此时队列已满,P2在#ref_p2处阻塞(同时释放queue上的锁)
4. C1继续执行,从队列中移除数据,并调用notify,假设该notify唤醒了C2,C2继续执行,由于此时队列已空,故C2在内循环中仍然阻塞在#ref_c1处
5. C1在下一循环中获取到queue上的锁,继续执行,由于此时队列为空,故和C2一样,阻塞在#ref_c1处(同时释放queue上的锁)
6. P1获取到queue上的锁,开始执行,由于此时队列为空,故P1往队列中插入一条数据,并调用notify,假设该notify唤醒了线程P2,P2从#ref_p2处继续开始执行,由于此时队列中有一条数据,即队列已满,故P2在内循环中仍然阻塞在#ref_p2处
7. P1在下一循环中执行到#ref_p1处,由于此时队列已满,故阻塞在#ref_p2处
到此时,4个线程都处于阻塞状态,任何一个都无法被唤醒,即发生了死锁
从示例中可见,notify导致死锁的原因时只能唤醒一个处于阻塞状态的线程,而解决死锁的方法也很简单,在第6步中如果调用了notifyAll,则会唤醒消费者线程,C1和C2被唤醒之后获取队列中的数据,使得流程可以继续进行下去,从而避免了死锁的发生