本篇博文主要分析条件锁的源码实现、以及状态两个队列的变化:
1)Condition的使用场景
2)lock方法的队列(FIFO双向无环链表)官方点说是同步队列 sync queue
3)condition队列(FIFO单向队列) 官方点说是条件队列 condition queue
4) await和signal方法被调用两个队列的变化图
本文是依赖于上篇博文Java锁Lock源码分析(一)在阅读本文之前强烈推荐先看下上一篇。
上篇主要主要是讲述了Lock的几个要点:
- state>0表示当前线程持有了锁,以及重入锁是如何表示的
2)并发的三个线程,获取锁过程、自旋的过程以及Node.waitStatus的状态图
3)源码讲解volatile、等待队列(FIFO的双向无环链表)的入队和出队
有了上一篇的基础再读本篇博文事半功倍,在说一点,本节主要以图的形式展开(以为代码很简答就是两个对列,准确来说是一个双向无环链表和一个单项无环链表的入队和出队操作,主要以图的形式看下就可以了)。
Condition跟Object的相似之处:
condition.await()类比Object的wait()方法,
condition.signal()类比Object的notify()方法,
conditionsignalAll()类比Object的notifyAll()方法。
先获取到锁,然后在判断条件是否满足,不满足则挂起,等待被唤醒
不同之处在于Object中的这些方法是需要跟同步监视器synchronized联合使用,而Condition是Lock配合使用。
Condition能更细粒度的控制线程的休眠与唤醒,对于同一个锁,我们可以创建多个Condition,来完成生产者和消费者的业务场景。
摘自 Doug Lea 的例子(网上到处是我直接拷贝过来),为了更好的说明我们本次以4个线程 thread1/thread2/thread3/thread4 来展开源码的分析。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class BoundedBuffer {
final Lock lock = new ReentrantLock();
// condition 依赖于 lock 来产生
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
// 生产
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 队列已满,等待,直到 not full 才能继续生产
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal(); // 生产成功,队列已经 not empty 了,发个通知出去
} finally {
lock.unlock();
}
}
// 消费
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 队列为空,等待,直到队列 not empty,才能继续消费
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal(); // 被我消费掉一个,队列 not full 了,发个通知出去
return x;
} finally {
lock.unlock();
}
}
}
这了已经展示了条件锁的使用,就是先lock()获取锁成功,让后再while(一定是while判断不能是if判断,不然只要不满足就永远不满足条件了)判断,不满足条件则condition.await();等待唤醒在此进入while判断条件,获取执行机会。
当向缓冲区中写入数据之后,唤醒”读线程”;
当从缓冲区读出数据之后,唤醒”写线程”,并且当缓冲区满的时候,”写线程”需要等待;
当缓冲区为空时,”读线程”需要等待。
如果采用Object类中的wait(), notify(), notifyAll()实现该缓冲区,当向缓冲区写入数据之后需要唤醒”读线程”时,不可能通过notify()或notifyAll()明确的指定唤醒”读线程”,而只能通过notifyAll唤醒所有线程(但是notifyAll无法区分唤醒的线程是读线程,还是写线程)。 但是,通过Condition,就能明确的指定唤醒读线程。
不知道大家还是是否有印象AQS的内部类Node
节点:
现在又多了一个内部类ConditionObject
,他是在成功获取锁了之后再看是否满足条件,不满足再次进入条件队列阻塞,满足的话执行相应的业务逻辑,两个对列是如何交互的下文会将。
再讲之前我们先模拟如下场景:
我们假定4个线程的并发执行lock.lock()方法执行的某个时间状态是如下的:
thread1成功获取了锁【lock()返回】,但是数组已经满了执行condition.await()【await()方法会将自己加入到条件队列并释放所后边代码会说明】,并将thread1挂起。
thread2在thread1释放锁后成功获取了锁【lock()返回】,数组也是满了执行condition.await(),正在释放锁,即thread2正在调用fullRelease()方法。
thread3和thread4都获取锁失败阻塞在lock方法中,在等待队列中。
lock()方法的在上一篇文章Java锁Lock源码分析(一)写过很详细,这里直接跳过看await()方法,它是AQS的另一个内部类ConditionObject的方法:
addConditionWaiter()就是创建一个条件队列的节点,
new Node(thread,Node.CONDITION)将waitStatus设置为-2,代码如下:
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;
}
这两个截图和上篇Java锁Lock源码分析(一) 就能得出如下的4个线程的状态:
thread2和thread3都是:
获取到了锁AQS的state>1
不满足执行业务逻辑条件创建条件Node,释放锁state归为0
我们thread2释放锁:
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;
}
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
释放锁的逻辑就是设置state=0,唤醒双向队列的下一个未取消的节点thread3,thread3的前一个就是head,所以再次执行tryAcquire(1)成功,将自己设置为头结点head这样队列头就被移除掉
,跟上一篇文章是一致的,,此时thread3获取锁成功,同样条件不满足进入await方法,两个队列的变化如下:
现在thread4和thread1线程都是挂起状态的,thread2(释放锁完毕)和thread3正在释放有可能正在挂起等,此时再有其它线程调用condition.signal()方法。我们在看下signal源码:
方法很简单就是将条件队列的第一个未取消的节点移除掉,设置waitStatus=0,并放入到等待(同步)队列中,将自己的waitStatus=-1,节点被唤醒执行一次执行await方法的后半部分所以会执行acquireQueued,将node.prev节点waitStatus=-1。
signalAll就是一次将所有的未取消的节点全部都挨个放入到等待队列中然后执行跟signal方法相同的逻辑。
然后等待thread4获取锁(等待thread3释放),thread4从同步队列移除,在创建条件节点进入到条件队列,直到thread1再次获取了退出了await方法中的acquireQueued方法,才表示thread1的await方法退出,可以执行业务逻辑了。
值得注意的是都是同步队列,thread1是在await方法中的acquireQueued方法阻塞,而thread4和thread5是在lock()方法中的acquireQueud方法中阻塞的,理解这个是非常重要的。
总的来说thread4和thread5是执行了两次acquireQueued,第一次是没有获取到锁,在队列自旋(不是一直在循环占用着CPU的之前文章说过),第二次是获取到锁,条件不满足,等满足被唤醒之后再执行acquireQueued。
这就是条件锁的整个的逻辑,剩下的流程就lock.unlock(); 对于图中表明的文字还是要看仔细,整个的流程还是比较复杂的。
在回顾一下之前的整个逻辑结合上一篇博文(理解这张图就真的明白条件锁了):
总结:
条件锁是有两个队列同步队列和条件队列
队列中存放的都是AQS的Node节点
同步队列主要使用node.prev和node.next waitStatus默认是0 等待获取锁waitStatus=-1,初始化一个空的head
条件队列主要是node.nextWaiter waitStatus=-2
lock方法阻塞的是进入到同步队列,await方法第一次阻塞是在条件队列(获取锁成功,但是不满足条件,需要由signal方法唤醒),第二次阻塞是在同步队列acquireQueue方法阻塞等待其他线程释放锁。
ps:上面的Doug Lea 大神的例子中,put和get方法都会调用了lock.lock()方法,这样能达到一种情景就是:put方法和put方法互斥,get和get方法互斥,put和get互斥,很显然get和get是互斥这样会损失了性能的,能否做到写方法和写方法互斥,读方法与写方法互斥,但是读方法与读方法不互斥。这就是读写锁来处理的事情了,下篇分析~!