生产者和消费者模式是并发编程中最常见的需要加锁的场景,既可以通过synchronized + 对象本身的监视器方法wait()、notify()、notifyAll()来实现等待/通知的机制,也可以通过ReentrantLock实现,而ReentrantLock方式可以结合Condition来实现等待/通知的机制。
建议:看这篇之前一定要先读懂第一篇,这样理解会容易的多
【并发编程】AQS源码分析(一) 从ReentrantLock来看AQS的基本数据结构和主要执行流程
下面生产者+消费者代码(ReentrantLock + Condition实现)
/**生产者消费者模式代码
提前说明,这里模拟的是队列,先进先出的模式
所以以数组为容器时,putIndex要按顺序放入,直到放满时从0开始继续放
takeIndex也要按顺序取出,直到最大索引位置取出时,再从0开始循环取出
**/
public class ArrayBlockingQueue<E> {
//存放元素的数组
final Object[] items;
//下次应该取的数组的索引,如果第一次取takeIndex = 0 的元素
//取完后,takeIndex++;即下次取takeIndex = 1的元素
int takeIndex;
//同上,将元素放入数组下标为putIndex的位置
int putIndex;
//当前数组中存放的元素的总量
int count;
//锁
final ReentrantLock lock;
//条件不再为空,当数组中有元素时,此条件成立
//数组不再为空时,可以继续从数组中取出元素
private final Condition notEmpty;
//条件不再满,当数组中元素不为数组最大值时,此条件成立
//数组不再满时,可以继续放入元素
private final Condition notFull;
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
//生产者
public boolean put(E e) throws InterruptedException {
checkNotNull(e);
//加锁
lock.lock();
try {
//当数组已经满时
while (count == items.length)
//数组不再满的条件要等待,此时不能放入元素
notFull.await();
//代码走到这里,说明数组元素没有满,向数组中放入一个元素
items[putIndex] = e;
//放入元素后,putIndex要加一,为下一次放入准备
if (++putIndex == items.length)
//如果putIndex加一后发现跟数组最大值相同,说明
//下次放入要开始循环从第一个元素的位置开始放了
putIndex = 0;
//放入元素后,count要自增1
count++;
//放入元素后,数组不再为空,通知消费者可以取元素了
notEmpty.signalAll();
return true;
} finally {
//解锁
lock.unlock();
}
}
//消费者
public E take() throws InterruptedException {
//加锁
lock.lock();
try {
//当数组中元素为空时
while (count == 0)
//不为空的条件等待,此时不能从数组中取元素
notEmpty.await();
//当走到这里时,说明数组中已经不再为空了
//取出一个元素
E x = (E) items[takeIndex];
//将相应位置为空
items[takeIndex] = null;
//取出一个元素是,下次取得元素时,takeIndex要自增1
if (++takeIndex == items.length)
//如果takeIndex自增1后 = 数组的最大长度
//那么下次取元素时takeIndex = 0
takeIndex = 0;
//取出元素后count--
count--;
//取出元素后,数组不再为满,此时可以通知生产者继续放入元素了
notFull.signalAll();
return x;
} finally {
lock.unlock();
}
}
private static void checkNotNull(Object v) {
if (v == null)
throw new NullPointerException();
}
/**
* Returns item at index i.
*/
@SuppressWarnings("unchecked")
final E itemAt(int i) {
return (E) items[i];
}
}
我们从代码中可以看出Condition是依靠于Lock产生的。所以,在使用Condition时,必须要对对象加锁,每个Lock可以实例化出多个Condition,从源码看,一个Conditon
对应的实例化的是一个ConditionObject.
//ReentrantLock类代码
final ConditionObject newCondition() {
return new ConditionObject();
}
来看ConditionObject结构
//ConditionObject是AQS的子类
public class ConditionObject implements Condition,
java.io.Serializable {
/** Node为AQS中的Node,
条件队列的第一个等待节点 */
private transient Node firstWaiter;
/** 条件队列的最后一个等待节点*/
private transient Node lastWaiter;
/**
* 构造方法
*/
public ConditionObject() { }
}
由源码可以看出,Condition其实也是由一个单向向链表构成,其中Node信息和AQS中的阻塞队列一致,二者共用Node。
把上篇文章中Node结构复制过来了如下:
//共享模式的节点,本篇文章不涉及
static final Node SHARED = new Node();
//独占模式
static final Node EXCLUSIVE = null;
//waitStatus状态之一:线程 取消 ,不再获取锁
static final int CANCELLED = 1;
//waitStatus状态之一 : 表示当前线程可以被前一个节点的线程唤醒,
//记住这唤醒是由阻塞队列中前一个节点来唤醒后一个节点的线程的
static final int SIGNAL = -1;
//waitStatus状态之一 :条件队列的状态
static final int CONDITION = -2;
//waitStatus状态之一
static final int PROPAGATE = -3;
//节点中线程等待的状态
volatile int waitStatus;
//前一个节点
volatile Node prev;
//后一个节点
volatile Node next;
//当前节点锁代表的线程
volatile Thread thread;
//条件队列中的等待线程
Node nextWaiter;
那么到此为止我们可以大致总结出Condition + AQS队列的结构了:
好了心中大概有了这个结构之后,再去看源码
/**可中断的阻塞方法
调用此方法后,满足条件后加入到对应的Condition队列中
等待被signal后继续执行
**/
public final void await() throws InterruptedException {
//判断当前线程是否中断
if (Thread.interrupted())
throw new InterruptedException();
//如果线程运行正常,将当前线程加入到条件队列中
Node node = addConditionWaiter();
//在调用await方法后,线程会释放锁,这里是释放锁的过程
int savedState = fullyRelease(node);
//是否中断,默认非中断
int interruptMode = 0;
/**
释放锁之后,这里要将当前线程挂起,挂起有两个条件
1、当前线程不在阻塞队列中 !isOnSyncQueue(node)
2、当前线程被中断了
**/
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
//当前线程被挂起了,挂起之后重新唤醒要从这里开始执行了
if ((interruptMode =
checkInterruptWhileWaiting(node)) != 0)
break;
}
/**退出循环了,这里说明已经退出挂起状态了,被其他线程唤醒了
acquireQueued这个方法很熟悉,将Node放入阻塞队列
开始去竞争锁了,返回true代表没有获取锁,
返回false代表获取到了锁
**/
if (acquireQueued(node, savedState) && interruptMode
!= THROW_IE)
//这里是当没有获取到锁,并且当前线程没有被中断时,
//设置为中断状态
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
//将Node添加进条件队列
private Node addConditionWaiter() {
Node t = lastWaiter;
// 如果最后一个node是取消状态,那么把它清除出去
if (t != null && t.waitStatus != Node.CONDITION) {
//将条件队列中从头到尾遍历
//将所有取消状态的Node清除出去
unlinkCancelledWaiters();
//将t作为最后一个条件队列的节点
t = lastWaiter;
}
//将当前线程包装成一个Node,状态为 -2
Node node = new Node(Thread.currentThread(),
Node.CONDITION);
if (t == null)
//如果队列为空,则node作为第一个节点
firstWaiter = node;
else
//否则node作为t(原来尾节点)的下一个节点
t.nextWaiter = node;
//将lastwaiter指向node
lastWaiter = node;
return node;
}
/**
在向条件队列中添加节点时,会将队列中已经取消的节点移除
将已经取消的线程移除条件队列,下面都是链表操作
**/
private void unlinkCancelledWaiters() {
//从头节点开始遍历,t代表每次要判断的节点
Node t = firstWaiter;
//临时节点,用来记录最后一个可用节点
Node trail = null;
while (t != null) {
//记录当前节点的next
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
//如果状态不是 -2,那么nextwaiter为null
//即将t断开连接清除出去
t.nextWaiter = null;
if (trail == null)
//此时如果trail为null,t为头节点的情况
//则让firstWaiter向后移动指向next
firstWaiter = next;
else
//如果trail不为null,t为中间节点的情况
//则直接跨过当前t指向next
trail.nextWaiter = next;
if (next == null)
/**如果next为null则lastwaiter
指向trail最后一个可用节点**/
lastWaiter = trail;
}
else
//如果符合条件,则trail指向t
//所以trail总是指向最后一个可用的节点
trail = t;
//继续判断下一个节点是否为 -2
t = next;
}
}
释放锁的过程。由于await时,必须是持有锁的状态才可以进行释放锁,由于锁重入的存在,释放锁时,有可能state = 1,也有可能state = n,所以,这里需要彻底的释放锁,无论是0还是n最后都应该state = 0;
//释放锁过程
//这里返回值saveState代表之前几次加锁的值
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
//这里还是调用了上篇文章中说的release方法,彻底解锁
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
//释放锁失败后,置为取消状态
node.waitStatus = Node.CANCELLED;
}
}
这里需要注意得是:
在阻塞队列中,node用到的是next,prev
而在条件队列中,node用的是nextwaiter,
在条件队列中没有进入到阻塞队列中时,next,prev应该是null
//判断node是否在阻塞队列中
//当返回false时,则线程挂起
final boolean isOnSyncQueue(Node node) {
//如果状态为 -2 或者 prev为空一定不在阻塞队列中
//在阻塞队列中prev一定不为null
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
/**
如果next 不为空,那么一定在阻塞队列中
**/
if (node.next != null)
return true;
/** 下面这个方法从阻塞队列的队尾开始从后往前遍历找,
如果找到相等的,说明在阻塞队列,否则就是不在阻塞队列
可以通过判断 node.prev() != null 来推断出 node 在阻塞队列吗?
答案是:不能。
这个可以看上篇 AQS 的入队方法,首先设置的是 node.prev 指向 tail,
然后是 CAS 操作将自己设置为新的 tail,可是这次的 CAS 是可能失败的。
**/
return findNodeFromTail(node);
}
//从阻塞队列的尾部开始向前遍历,寻找node是否在阻塞队列中
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
代码走到这里,如果isOnSyncQueue返回false时,则线程挂起,此时就需要等待其他线程的signal了。
好了,到这里先来总结一下await方法的执行流程吧如下图:
好了,await先到这里,我们先看signal方法,完了以后再看唤醒之后的流程吧
//signal线程
public final void signal() {
//唤醒线程时,唤醒的线程必须拥有独占锁,
//也就是你唤醒别人的条件是
//你必须先拥有锁才可以唤醒别人
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//唤醒的是第一个节点,等待最久的
Node first = firstWaiter;
if (first != null)
//唤醒操作
doSignal(first);
}
//这个方法是要将第一个条件队列中的节点转移并唤醒的操作
private void doSignal(Node first) {
do {
/**
1、firstWaiter 指向first节点的nextwaiter
**/
if ( (firstWaiter = first.nextWaiter)
== null)
/**
2、此时如果nextWaiter为null,
那么说明队列为空了
3、所以让lastWaiter指向null
**/
lastWaiter = null;
//走到这里,说明nextWaiter不为null,
//那么让first节点和原来的队列断开,
//因为他要转移走了
first.nextWaiter = null;
/**
transferForSignal(first)是要将first节点转移并唤醒
当没有转移并唤醒成功时,让first继续指向nextwaiter(
在do里面,已经把firstWaiter指向了nextWaiter了)
继续循环上面的逻辑直到能转移并成功唤醒下一个节点为止
**/
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
//将节点node转移进阻塞队列并在符合条件时直接唤醒
final boolean transferForSignal(Node node) {
/*
* 将节点的状态设置为 0
* 这个是进入阻塞队列中的初始条件
*/
if (!compareAndSetWaitStatus(node,
Node.CONDITION, 0))
return false;
/*
这个方法上篇文章说过了,
是通过CAS并自旋将node加入到阻塞队列的对尾
返回的Node p是node节点的前驱节点
*/
Node p = enq(node);
int ws = p.waitStatus;
/**
之前说过,当前节点的唤醒要由前驱节点的状态来决定
ws > 0 说明前驱节点已取消
!compareAndSetWaitStatus(p, ws, Node.SIGNAL)
说明设置前节点唤醒状态失败
**/
if (ws > 0 || !compareAndSetWaitStatus(p, ws,
Node.SIGNAL))
//如果前节点不符合唤醒条件,
//则直接唤醒当前节点中的线程
LockSupport.unpark(node.thread);
//否则,如果前节点符合条件的话,则不会唤醒当前节点,
//当前节点直接进入阻塞队列
return true;
}
代码进行到了这里,node节点就已经从条件队列进入到了阻塞队列中去了。也就相当于重新唤醒了。
那么节点在唤醒后要沿着挂起之后的代码继续执行。
唤醒之后继续执行代码,是先进行了检查中断状态
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
//线程挂起了
LockSupport.park(this);
/**
挂起之后重新唤醒之后继续执行的地方
**/
if ((interruptMode =
checkInterruptWhileWaiting(node)) != 0)
break;
}
接着往下看
//检查waiting期间中断状态方法
/**
这里涉及到了两个中断状态 THROW_IE REINTERRUPT
如果是在被唤醒前打断,则返回 THROW_IE = -1
如果是在被唤醒之后打断的,则返回 REINTERRUPT = 1
**/
private int checkInterruptWhileWaiting(Node node) {
//只有线程中断了 Thread.interrupted()
//才会返回true
return Thread.interrupted() ?
//只有中断了,才会调用此方法
//transferAfterCancelledWait
(transferAfterCancelledWait(node) ?
THROW_IE :
REINTERRUPT) :
0;
}
//判断线程是signal前中断还是signal之后中断
//如果是signal之前中断返回true
//如果是signal之后中断返回false
final boolean transferAfterCancelledWait(Node node) {
//如果CAS设置成功,说明signal之前中断
//否则状态在进入阻塞之后已经被修改为了0了
if (compareAndSetWaitStatus(node, Node.CONDITION,
0)) {
//加入阻塞队列
enq(node);
return true;
}
/*
*如果cas失败遍历阻塞队列判断node是否在阻塞队列中
这里用while一直循环判断,个人理解
为了等待node被signal之后进入
阻塞队列中
*/
while (!isOnSyncQueue(node))
//如果一直不在阻塞队列中,
//则让出cpu直到进入阻塞队列为止
Thread.yield();
return false;
}
好了,检查中断的方法已经执行完了,那么继续回去看挂起之后的方法,容易晕,一定要跟着代码一步一步来
又回到了这里
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
//线程挂起了
LockSupport.park(this);
if ((interruptMode =
checkInterruptWhileWaiting(node)) != 0)
//检查中断后发现线程被中断了,则直接跳出循环
//这里很重要
//从上面代码我们可以知道,跳出循环时,
// ====node已经进入了阻塞队列 =====
break;
}
那么继续回到await()方法里面,跳出循环后继续向下执行。上面已经说过了,即使线程被中断了,被中断的node在signal检查中断的时候也会加入到阻塞队列中去。所以在跳出循环后,开始去尝试获取锁了。
/**
acquireQueued在上篇文章中说过是自旋获取锁并根据
状态判断是否要进行线程挂起 如果返回true说明线程中断,
返回false说明获取锁成功
savedState这里是await方法中完全释放锁之后的state的值
**/
if (acquireQueued(node, savedState) &&
interruptMode != THROW_IE)
//如果获取锁失败,线程中断了,
//并且interruptMode是signal之后中断的
//那么重新抛出中断异常
interruptMode = REINTERRUPT;
//???这里我自己一直没弄明白为什么要判空
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
//处理中断
reportInterruptAfterWait(interruptMode);
//处理中断
private void reportInterruptAfterWait(
int interruptMode)
throws InterruptedException {
//如果是signal之前发生的中断,直接抛出异常
if (interruptMode == THROW_IE)
throw new InterruptedException();
//如果signal之后发生中断,则自我中断
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
唤醒及唤醒之后的流程就到这里基本上就结束了。用一张图来总结一下。
以上是关于signal和await的源码过程,有一些细节自己也不是特别清楚。
但是了解了这些代码之后,再看其他的方法的源码就会轻松很多了。