基于synchronized的生产者/消费者模型
其原理就是通过在获取到锁的wait/notify来实现的线程通信。
@SuppressWarnings("all")
public class ProviderConsumerWithSync {
private Queue<Integer> queue = new LinkedList<>();
public static void main(String[] args) {
ProviderConsumerWithSync test = new ProviderConsumerWithSync();
// 三个消费者 一个生产者
for (int i =0; i < 3; i++) {
new Consumer(test.queue, String.format("消费线程_%d",i )).start();
}
LockSupport.parkNanos(2000 * 1000 * 1000);
// 生产者启动
for (int i = 0; i < 3; i++) {
}
new Provider(test.queue, String.format("生产线程_%d", 1)).start();
}
static class Consumer extends Thread{
private Queue queue;
public Consumer(Queue queue, String name) {
super(name);
this.queue = queue;
}
@Override
public void run() {
while (true) { // 线程一直去工作
synchronized (queue) { //对队列加锁
while (queue.size() <= 0) { // while判断而不是if判断防止虚假唤醒
System.out.println(Thread.currentThread().getName() + "被阻塞,等待生产");
try {
queue.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
// 当队列不为空
System.out.println(Thread.currentThread().getName() + "从队列中消费一个 "+ queue.remove());
// 唤醒生产线程进行生产
queue.notifyAll(); // 这里用notify去防止陷入死等待。
}
}
}
}
static class Provider extends Thread{
private Queue queue;
public Provider(Queue queue, String name) {
super(name);
this.queue = queue;
}
@Override
public void run() {
while (true) { // 线程一直去工作
synchronized (queue) {
while (queue.size() > 10) {
System.out.println(Thread.currentThread().getName() + "队列达到阈值,被阻塞,等待消费");
// 达到上限
try {
queue.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " 开始生产");
queue.add(new Integer(1));
LockSupport.parkNanos(2000_000_000);
// 去唤醒消费线程去消费
queue.notifyAll();
}
}
}
}
}
基于Lock和Condition的生产者/消费者模型
和synchronized一样,这里是用ReentrantLock和await/signalAll来配合使用。这里要注意是通过condition创建生产者生产和消费者消费的await条件,并且这两个条件Consumer和Provider都要持有,比如对于Provider来说,需要在生产到阈值时调用生产者Condition的await进行等待,而在生产完毕之后需要调用消费者的condition的signalAll方法来唤醒在此条件上等待的消费者。
@SuppressWarnings("all")
public class ProviderConsumerWithLock {
private Lock lock = new ReentrantLock();
private Queue<Integer> queue = new LinkedList<>();
private Condition consumer = lock.newCondition();
private Condition provider = lock.newCondition();
public static void main(String[] args) {
ProviderConsumerWithLock test = new ProviderConsumerWithLock();
// 三个消费者 一个生产者
for (int i =0; i < 3; i++) {
new ProviderConsumerWithLock.Consumer(test, String.format("消费线程_%d",i )).start();
}
LockSupport.parkNanos(2000 * 1000 * 1000);
// 生产者启动
for (int i = 0; i < 3; i++) {
}
new ProviderConsumerWithLock.Provider(test, String.format("生产线程_%d", 1)).start();
}
static class Provider extends Thread {
private Lock lock;
private Queue queue;
private Condition provider;
private Condition consumer;
public Provider(ProviderConsumerWithLock lock, String name) {
super(name);
this.lock = lock.lock;
this.queue = lock.queue;
this.provider = lock.provider;
this.consumer = lock.consumer;
}
@Override
public void run() {
while (true) { // 一直工作
lock.lock();
try {
while (queue.size() > 10) { // while防止虚假唤醒
System.out.println(Thread.currentThread().getName() + "队列达到阈值,被阻塞,等待消费");
// 生产的条件await
provider.await();
}
System.out.println(Thread.currentThread().getName() + " 开始生产");
queue.add(new Integer(1));
Thread.sleep(500);
consumer.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
static class Consumer extends Thread {
private Lock lock;
private Queue queue;
private Condition provider;
private Condition consumer;
public Consumer(ProviderConsumerWithLock lock, String name) {
super(name);
this.lock = lock.lock;
this.provider = lock.provider;
this.consumer = lock.consumer; this.queue = lock.queue;
}
@Override
public void run() {
while (true) {
lock.lock();
try {
while (queue.size() <= 0) { // while防止虚假唤醒
System.out.println(Thread.currentThread().getName() + "被阻塞,等待生产");
// 消费的条件被阻塞
consumer.await();
}
// 当队列不为空
System.out.println(Thread.currentThread().getName() + "从队列中消费一个 "+ queue.remove());
// 唤醒生产线程进行生产
provider.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
}
基于锁和线程通讯方案的两个问题
可以先看看线程通讯:
- 线程在竞争锁失败的时候会进入Entry Set中,图中2可以表示获取锁
- 调用线程的wait()方法让线程阻塞,线程就会放在Wait Set中(图中3即为调用wait之后释放锁)。而Wait Set中的线程可以到wait时间或者被notify之后获取锁(如图4)。
- notify和notiifyall就是将wait set中的线程重新唤醒,放入到entry set中重新竞争锁。
虚假唤醒
即指的wait方法的调用在对 生产者阈值(比如队列满了)/消费者阈值(比如队列为空)条件判断是if的情况下,产生的虚假唤醒问题。
比如对于消费者来说:
... 省略代码
synchronized (queue) {
if (queue.size() <= 0) { // 这里不是while 而是if
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("consumer remove: " + queue.remove() + ": " + Thread.currentThread().getName());
queue.notifyAll();
}
... 省略代码
如果有两个消费者线程来消费,先后都获取了锁,然后进入了wait方法等待,等再次被唤醒的时候如果没有再次判断可能会出现错误的操作(比如此时虽然被唤醒,但队列还是空,调用remove方法就会报错。)
所以这里要记得用while判断,防止不满足条件的虚假唤醒。
notify死锁问题
我们知道notify是随机唤醒一个线程去重新竞争锁。
考虑一种场景:队列大小是1,有一个生产者叫做p,两个消费者叫做c1、c2; c1和c2先启动,由于队列为空,所在都wait在wait set; p随后生产了一个消息调用notify,然后由于队列由于队列满了所以wait在wait set;假设p随机唤醒的是c1,那么c1消费完队列中的一个消息后会notify,notify会随机选择一个wait set中的线程,注意,此时c2和p都在wait set,假设唤醒的是c2,则c2由于检查到队列为空则wait,p则无人再唤醒了,这个时候就产生了死锁。如果是调用的notifyAll的话,则p和c2都会被唤醒,就会避免这种情况的发生。
所以synchronized这种没办法精准控制等待条件的锁,需要调用notifyAll来防止死锁问题。
基于阻塞队列实现的生产者/消费者模型
BlockingQueue内部已经实现了同步的队列,实现方式为await和signal
- put方法 容量到达最大时,自动阻塞
- take方法 容量为0时,自动阻塞
所以线程池也是经典通过阻塞队列实现的生产者-消费者模式。
比如put方法调用的putLast方法
public void putLast(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
Node<E> node = new Node<E>(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 在其中调用了notEmpty的singal方法
while (!linkLast(node))
notFull.await();
} finally {
lock.unlock();
}
}
private boolean linkLast(Node<E> node) {
// assert lock.isHeldByCurrentThread();
if (count >= capacity)
return false;
Node<E> l = last;
node.prev = l;
last = node;
if (first == null)
first = node;
else
l.next = node;
++count;
notEmpty.signal();
return true;
}