为什么多生产消费者要使用notifyAll而不是notify
Posted 小猪快跑22
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么多生产消费者要使用notifyAll而不是notify相关的知识,希望对你有一定的参考价值。
notify 和 notifyAll 的区别?
1.线程调用了wait()方法,便会释放锁,并进入等待池中,不会参与锁的竞争
2. 调用notify()后,等待池中的某个线程(只会有一个)会进入该对象的锁池中参与锁的竞争,若竞争成功,获得锁,竞争失败,继续留在锁池中等待下一次锁的竞争。
3.调用notifyAll()后,等待池中的所有线程都会进入该对象的锁池中参与锁的竞争。
锁池和等待池
锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块)时就必须先获得该对象的锁,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.
=注意=,执行notify方法后并不是立马释放锁,而是在synchronized方法或synchronized代码块执行完成之后才会释放。
如下例子:
Object lock = new Object();
Thread waitThread = new Thread("waitThread") {
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName()+" start");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" end ");
}
}
};
Thread notifyThread = new Thread("notifyThread") {
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName()+" notify start");
lock.notify();
System.out.println(Thread.currentThread().getName()+"notify ----");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"notify end");
}
}
};
waitThread.start();
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
notifyThread.start();
运行结果如下:
waitThread start
notifyThread notify start
notifyThreadnotify ----
notifyThreadnotify end
waitThread end
可以看到 并不是 调用notify后立马就释放锁,而是 synchronized 代码块执行完才会释放锁。
单生产、消费者使用 notify
消费者
public class Consumer implements Runnable{
private final Object lock;
private List<String> data;
public Consumer(Object lock, List<String> data) {
this.lock = lock;
this.data = data;
}
private void get() {
synchronized (lock) {
try {
while (data.size() == 0) {
lock.wait();
}
String val = data.remove(0);
System.out.println(Thread.currentThread().getName() + " 消费了数据 " +val);
lock.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
for (int i = 0; i < 4; i++) {
get();
}
}
}
生产者
public class Producer implements Runnable{
private final Object lock;
private List<String> data;
public Producer(Object lock, List<String> data) {
this.lock = lock;
this.data = data;
}
private void put() {
synchronized (lock) {
try {
while (data.size() == 1) {
lock.wait();
}
int random = new Random().nextInt(100);
System.out.println(Thread.currentThread().getName() + " 生产了数据 " + random);
data.add(String.valueOf(random));
lock.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
int i = 0;
while (i < 4) {
i++;
put();
}
}
}
测试代码:
Object lock = new Object();
List<String> list = new ArrayList<>();
new Thread(new Consumer(lock, list), "消费者线程1 >>> ").start();
// new Thread(new Consumer(lock, list), "消费者线程2 >>> ").start();
new Thread(new Producer(lock, list), "生产者线程 ---").start();
结果如下:
生产者线程 --- 生产了数据 42
消费者线程1 >>> 消费了数据 42
生产者线程 --- 生产了数据 69
消费者线程1 >>> 消费了数据 69
生产者线程 --- 生产了数据 9
消费者线程1 >>> 消费了数据 9
生产者线程 --- 生产了数据 87
消费者线程1 >>> 消费了数据 87
从结果上看是没问题的,下面看看多生产、消费者模式
2个生产者2个消费者
其他的代码不变,只改变测试代码;结果发现执行到下面就出现一种假死状态了。
生产者线程1 --- 生产了数据 42
消费者线程1 >>> 消费了数据 42
生产者线程2 --- 生产了数据 6
为什么会出现这种状态呢?原因如下:
假设 消费者线程1 ---- A
消费者线程2 ---- B
生产者线程1 ---- C
生产者线程2 ---- D
- 假设 A 先获取锁,发现集合 data 里面没有数据 然后 调用 wait 且释放锁
- 假设 B 获取了锁,同样,发现集合 data 里面没有数据 然后 调用 wait 且释放锁
- 假设 C 先获取锁,然后生产了一条数据存入data,然后调用 notify()去唤醒处于等待池中的线程之一(A 或者 B),接着执行循环,发现data 满了,调用 wait 且释放锁。
- 假设 还是 A 先获取了锁,那么 A 消费了数据,然后调用 notify,接着去执行循环,此时发现 data 空了,调用wait 方法且释放锁。 此时A 、B、C都处于等待池。
- 由于A、B、C都处于等待池了,那么此时 D 会获取锁,D 生产一条数据,然后调用 notify,接着去循环,然后发现 data 满了,然后调用 wait 且释放锁。
- 这时,线程C再次获取了锁,然后发现 data 满了,再次 调用 wait 方法,就会出现 A 、B、C、D 4个线程都处于wait状态了,处于一种假死等待状态。
现在上面的代码 生产者和消费者中的 notify改为 notifyAll() ,那么问题都解决了,结果如下:
生产者线程1 --- 生产了数据 67
消费者线程2 >>> 消费了数据 67
生产者线程2 --- 生产了数据 60
消费者线程1 >>> 消费了数据 60
生产者线程1 --- 生产了数据 64
消费者线程2 >>> 消费了数据 64
生产者线程2 --- 生产了数据 18
消费者线程1 >>> 消费了数据 18
生产者线程1 --- 生产了数据 81
消费者线程2 >>> 消费了数据 81
生产者线程2 --- 生产了数据 59
消费者线程1 >>> 消费了数据 59
生产者线程1 --- 生产了数据 19
消费者线程2 >>> 消费了数据 19
生产者线程2 --- 生产了数据 42
消费者线程1 >>> 消费了数据 42
注意,这里面是2个生产者和2个消费者,如果是1个生产者2个消费者,且里面的循环次数都是一样的话(例子中都是4),那么你会发现也会出现假死现象,原因都是一样的,有兴趣的自己分析下哈。
===但是,现在一般都喜欢用 ReentrantLock 以及 Condition来实现,可以看到Java的好多队列的源码都是这样用的,且不需要调用 signalAll ===
为什么不用signalAll呢?###
因为这里面是可以定向唤醒的,即生产者只会唤醒消费者,消费者只会唤醒生产者,不会发生消费者唤醒另外的消费者。
我也写了例子,如下:
生产者
public class Producer2 implements Runnable{
private final ReentrantLock lock;
private Condition notEmpty;
private Condition notFull;
private List<String> data;
public Producer2(ReentrantLock lock, List<String> data, Condition notFull, Condition notEmpty) {
this.lock = lock;
this.data = data;
this.notEmpty = notEmpty;
this.notFull = notFull;
}
private void put() {
try {
// System.out.println(Thread.currentThread().getName() + "去生产");
lock.lock();
while (data.size() == 1) {
notFull.await();
}
int random = new Random().nextInt(100);
System.out.println(Thread.currentThread().getName() + " 生产了数据 " + random);
data.add(String.valueOf(random));
notEmpty.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
@Override
public void run() {
int i = 0;
while (i < 10) {
i++;
put();
}
}
}
消费者
public class Consumer2 implements Runnable {
private final ReentrantLock lock;
private Condition notEmpty;
private Condition notFull;
private List<String> data;
public Consumer2(ReentrantLock lock, List<String> data, Condition notFull, Condition notEmpty) {
this.lock = lock;
this.data = data;
this.notEmpty = notEmpty;
this.notFull = notFull;
}
private void get() {
// System.out.println(Thread.currentThread().getName() + "去消费");
try {
lock.lock();
while (data.size() == 0) {
notEmpty.await();
// System.out.println(Thread.currentThread().getName() + "被唤醒");
}
String val = data.remove(0);
System.out.println(Thread.currentThread().getName() + " 消费了数据 " + val);
notFull.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
get();
}
}
}
调用的地方:
private static void test2() {
ReentrantLock reentrantLock = new ReentrantLock();
Condition notFull = reentrantLock.newCondition();
Condition notEmpty = reentrantLock.newCondition();
List<String> list = new ArrayList<>();
new Thread(new Consumer2(reentrantLock, list, notFull, notEmpty), "消费者线程1 >>> ").start();
new Thread(new Producer2(reentrantLock, list, notFull, notEmpty), "生产者线程1 >>> ").start();
new Thread(new Consumer2(reentrantLock, list, notFull, notEmpty), "消费者线程2 >>> ").start();
new Thread(new Producer2(reentrantLock, list, notFull, notEmpty), "生产者线程2 >>> ").start();
}
结果如下:
生产者线程1 >>> 生产了数据 38
消费者线程2 >>> 消费了数据 38
生产者线程2 >>> 生产了数据 39
消费者线程1 >>> 消费了数据 39
生产者线程1 >>> 生产了数据 56
消费者线程2 >>> 消费了数据 56
生产者线程2 >>> 生产了数据 3
消费者线程1 >>> 消费了数据 3
生产者线程1 >>> 生产了数据 91
消费者线程2 >>> 消费了数据 91
生产者线程2 >>> 生产了数据 27
消费者线程1 >>> 消费了数据 27
生产者线程1 >>> 生产了数据 87
消费者线程2 >>> 消费了数据 87
生产者线程2 >>> 生产了数据 46
消费者线程1 >>> 消费了数据 46
生产者线程1 >>> 生产了数据 61
消费者线程2 >>> 消费了数据 61
生产者线程2 >>> 生产了数据 55
消费者线程1 >>> 消费了数据 55
生产者线程1 >>> 生产了数据 30
消费者线程2 >>> 消费了数据 30
生产者线程2 >>> 生产了数据 90
消费者线程1 >>> 消费了数据 90
生产者线程1 >>> 生产了数据 79
消费者线程2 >>> 消费了数据 79
生产者线程2 >>> 生产了数据 80
消费者线程1 >>> 消费了数据 80
生产者线程1 >>> 生产了数据 63
消费者线程2 >>> 消费了数据 63
生产者线程2 >>> 生产了数据 24
消费者线程1 >>> 消费了数据 24
生产者线程1 >>> 生产了数据 54
消费者线程2 >>> 消费了数据 54
生产者线程1 >>> 生产了数据 22
消费者线程1 >>> 消费了数据 22
生产者线程2 >>> 生产了数据 11
消费者线程1 >>> 消费了数据 11
生产者线程2 >>> 生产了数据 6
消费者线程2 >>> 消费了数据 6
以上是关于为什么多生产消费者要使用notifyAll而不是notify的主要内容,如果未能解决你的问题,请参考以下文章