wait和notify实现生产者消费者模式
讲Condition之前,有必要再熟悉下wait和notify结合synchronized实现线程的通信,比如实现生产者和消费者模式。案例代码如下。
public class ProductConsumer {
private int queueSize = 10;
private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
public static void main(String[] args) throws InterruptedException {
ProductConsumer test = new ProductConsumer();
Thread producer = test.new Producter();
Thread consumer = test.new Consumer();
producer.start();
consumer.start();
}
//生产者线程
class Producter extends Thread {
@Override
public void run() {
while (true) {
//同步代码块,获取队列锁
synchronized (queue) {
//当队列不满时生产者可以继续生产,生产之后唤醒消费者
//唤醒消费者,在生产者释放锁之后,消费者不一定就会获取锁,也许是生产者获取到锁继续执行
//但是如果不唤醒生产者,当队列满时,如果消费者处于阻塞状态,那么生产者和消费者都处于阻塞状态,程序就无法继续执行
if (queue.size() < queueSize) {
queue.add(queue.size() + 1);
System.out.println("生产者向队列中加入产品P,队列剩余空间:" + (queueSize - queue.size()));
try {
//模拟生产者生产过程,sleep不会释放锁
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒消费者
queue.notify();//1)随机生产和消费
} else {
try {
System.out.println("队列已满等待消费者消费");
//队列已满,进入阻塞状态,等待消费者消费
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
queue.notify();
}
}
}
}
}
}
//消费者线程
class Consumer extends Thread {
@Override
public void run() {
while (true) {
//同步代码块,获取队列锁
synchronized (queue) {
//如果队列是空的,消费者进入阻塞状态,等待生产者生产并唤醒
if (queue.isEmpty()) {
System.out.println("没有产品可以消费,进入阻塞状态等待生产者生产。");
try {
//进入阻塞状态释放队列锁,因为只有两个线程,所以生产者一定会获取到队列锁执行
queue.wait();//1)随机生产和消费
} catch (InterruptedException e) {
e.printStackTrace();
//如果发送异常,主动唤醒生产者线程执行
queue.notify();
}
//System.out.println("消费者获取到队列锁准备消费");
} else {
//如果队列不空,就消费产品,并唤醒生产者
//注意唤醒生产者,在消费者执行完毕释放锁之后,不一定生产者就会获得锁,也许消费者会继续获取锁执行
//但是如果不唤醒生产者,那么如果生产者处于阻塞状态,当队列为空,消费者也进入阻塞状态那么就没有线程可以获取锁继续执行了
queue.notify();//1)随机生产和消费
try {
//模拟消费者消费过程,sleep不会释放锁
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
queue.poll();
System.out.println("消费者消费了产品P,剩余空间:" + (queueSize - queue.size()));
}
}
}
}
}
}
执行结果:
生产者向队列中加入产品P,队列剩余空间:9
消费者消费了产品P,剩余空间:10
没有产品可以消费,进入阻塞状态等待生产者生产。
生产者向队列中加入产品P,队列剩余空间:9
消费者消费了产品P,剩余空间:10
生产者向队列中加入产品P,队列剩余空间:9
通俗的理解就是,厨师不停的炒菜并将炒好的菜放入厨柜,服务员不停的从厨柜里端菜。过程1:假设,服务员把菜端完了发现厨柜里已经空了,他会去等待(this.wait),直到厨师炒好菜放入厨柜并通知他。
过程2:服务员在等待的过程中,厨师继续不停的炒菜,然后厨柜被放满了,这时候就通知服务员可以端菜了(this.notify)。
过程3:过程1和过程2是两种极端的情况,其实生活中很少出现厨柜菜都放满了还不见服务员端菜的。常见的情况都是服务员时不时看下是不是有炒好的菜了,有的话就端出去。java线程也是一样,生产者线程和消费者线程执行都是随机的,谁先获得锁谁先执行,另一个就阻塞。
Condition用法
还记得之前说的synchronized和lock吗?这两者的关系就好比【wait和notify】和condition。只不过condition和lock一样都是基于java来实现,我们可以深入到源码来一探究竟。
ConditionWait.java
public class ConditionWait implements Runnable {
private Lock lock;
private Condition condition;
public ConditionWait(Lock lock, Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
try {
lock.lock();
try {
System.out.println("ConditionWait开始阻塞...");
condition.await();
System.out.println("ConditionWait结束阻塞...");
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
}
ConditionNotify.java
public class ConditionNotify implements Runnable {
private Lock lock;
private Condition condition;
public ConditionNotify(Lock lock, Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
try {
lock.lock();
System.out.println("ConditionNotify开始唤醒...");
condition.signal();
System.out.println("ConditionNotify结束唤醒...");
} finally {
lock.unlock();
}
}
}
ConditionTest.java
public class ConditionTest {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
ConditionWait conditionWait = new ConditionWait(lock, condition);
ConditionNotify conditionNotify = new ConditionNotify(lock, condition);
new Thread(conditionWait).start();
new Thread(conditionNotify).start();
}
}
执行结果:
ConditionWait开始阻塞...
ConditionNotify开始唤醒...
ConditionNotify结束唤醒...
ConditionWait结束阻塞...
Condition原理分析
上面的condition代码用法跟wait和notify用法差不多,现在就来分析它的原理。分析原理还是跟之前的AQS一样,使用情节来描述整个执行过程。
情节1: 上面的两个类对应的线程这里直接称呼wait线程和notify线程,一开始这两个线程都是执行lock.lock();两个线程都去争抢锁,假设wait线程优先获得锁,那么应该是这个样子。
情节2: 既然notify线程争抢锁失败,那么就会封装成Node加入到同步队列里。
【同步队列】
情节3: wait线程获得锁后执行代码condition.await();这里wait线程将会阻塞。阻塞之后将会怎么办呢?
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
源码里跟AQS很相似,这里会将wait线程封装成Node节点,并且头结点和尾结点都指向它,只不过这里的头结点和尾结点使用firstWaiter和lastWaiter来表示了。假设还有线程C也执行了condition.await();阻塞,那么就会追加在wait线程的后面,过程大概是这个样子。
【等待队列】
这个队列跟AQS队列的不同在于,这个是单向链表,称之为等待队列,AQS称之为同步队列。
情节4: 我们知道wait线程阻塞了就需要释放锁,而前面争抢锁失败的notify线程就会从同步队列里被唤醒。当执行到代码:condition.signal();时,意味着wait线程又会被唤醒了,但是唤醒是在同步队列里进行的,所以这里会先将处于等待队列的wait线程转移到同步队列里并唤醒。
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
// 当前节点从等待队列转移至同步队列
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
总结
关于condition的原理会有两种队列出现,一种是AQS同步队列,另一种是condition等待队列。首先是在多个线程争抢锁的时候,对于一些获得锁失败的线程需要加入到同步队列里,而对于主动调用await方法阻塞的线程,则会放入到等待队列里。当线程调用signal方法时,处于等待队列里的线程才会被唤醒,但是由于唤醒线程只能在AQS同步队列里进行,所以还需要先将等待队列里的最先一个线程转移至同步队列里继续等待,等真正轮到它了,才会被唤醒。