AQS之独占模式和共享模式
由于ReentrantLock是一个独占锁,独占锁的知识可以参考AQS之理论知识(一)和AQS之公平锁和非公平锁(二) 两篇文章,本文重点讲解共享模式。并且本文共享模式的讲解以CountDownLatch为主。
一、概念
AQS提供了两种工作模式:独占(exclusive)模式和共享(shared)模式。它的所有子类中,要么实现并使用了它独占功能的 API,要么使用了共享功能的API,而不会同时使用两套 API,即便是它最有名的子类 ReentrantReadWriteLock,也是通过两个内部类:读锁和写锁,分别实现的两套 API 来实现的。
独占模式即当锁被某个线程成功获取时,其他线程无法获取到该锁,共享模式即当锁被某个线程成功获取时,其他线程仍然可能获取到该锁。
1.1 独占模式
同一时间只有一个线程能拿到锁执行,锁的状态只有0和1两种情况。
1.2 共享模式
同一时间有多个线程可以拿到锁协同工作,锁的状态大于或等于0。
1.3 API对比
独占模式 | 共享模式 |
---|---|
tryAcquire(int arg) | tryAcquireShared(int arg) |
acquire(int arg) | acquireShared(int arg) |
acquireQueued(final Node node, int arg) | doAcquireShared(int arg) |
tryRelease(int arg) | tryReleaseShared(int arg) |
release(int arg) | releaseShared(int arg) |
二、AQS重要方法
2.1 acquireShared
该方法的调用链:tryAcquireShared->doAcquireShared。其中tryAcquireShared由子类根据不同的业务需求实现,在Semaphore重要方法具体分析。doAcquireShared在AQS中已经实现,直接调用即可。
/**
* Acquires in shared uninterruptible mode.
* @param arg the acquire argument
*/
private void doAcquireShared(int arg) {
/*设置节点为共享模式,并插入队尾*/
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
/*获取前驱节点,如果为头节点的话,则尝试获取锁*/
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
/*r>=0代表获取锁成功,线程被唤醒*/
if (r >= 0) {
/*设置新的头节点并且唤醒同步队列中的线程*/
setHeadAndPropagate(node, r);
p.next = null; // help GC
/*检测线程是否被中断过,如果中断过则再次调用中断逻辑*/
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
doAcquireShared方法的主要路径是addWaiter->tryAcquireShared->setHead()->doReleaseShared(),对应下面的业务大概:
2.1.1 线程信息包装成节点并添加到队尾;
2.1.2 竞争锁资源
2.1.3 获取节点头结点并 放在队列第一个
2.1.4 唤醒当前节点后的节点,如果队列头部被修改,循环唤醒下一个。
2.2 releaseShared
该方法的调用链:tryReleaseShared->doReleaseShared。其中tryReleaseShared由子类根据不同的业务需求实现,在Semaphore重要方法具体分析。doReleaseShared在AQS中已经实现,直接调用即可。
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
// 无限循环
for (;;) {
// 保存头结点
Node h = head;
if (h != null && h != tail) { // 头结点不为空并且头结点不为尾结点
// 获取头结点的等待状态
int ws = h.waitStatus;
if (ws == Node.SIGNAL) { // 状态为SIGNAL
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 不成功就继续
continue; // loop to recheck cases
// 释放后继结点
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) // 状态为0并且不成功,继续
continue; // loop on failed CAS
}
if (h == head) // 若头结点改变,继续循环
break;
}
}
releaseShared实现了加速唤醒,主要靠h == head
这段逻辑实现,例举一种doReleaseShared
中h == head
不成立的场景,用户层面的线程a释放锁之后,位于队首的线程t1被唤醒,t1调用setHeadAndPropagate
方法设置头节点为t1,但还未调用doReleaseShared
中的unparkSuccessor
方法,这时用户层面的线程b释放锁,唤醒位于队首的线程t2,t2调用setHeadAndPropagate
设置新的头节点为t2,这个时候t1继续执行,最后发现队首元素已经变化,继续for循环调用unparkSuccessor
方法唤醒队首元素。
doReleaseShared
中h == head
不成立时进入for循环持续唤醒同步队列中线程的逻辑,主要是一种加速唤醒的优化逻辑,当头节点发生变化时,说明此时有不止一个线程释放锁,而在共享模式下,锁是能够被不止一个线程所持有的,因此应该趋向于唤醒更多同步队列中的线程来获取锁。
三、CountDownLatch重要方法
3.1 概念
CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。
CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。
3.2 属性
3.2.1 Sync类
帮助类,同时也是一个抽象类,继承了AQS,同时又根据需要添加了一些方法,供CountDownLatch调用。
3.2.1.1 Sync() 构造方法
//初始化数值用于计数
Sync(int count) {
setState(count);
}
3.2.1.2 tryAcquireShared
//如果state值等于,说明锁有效,否则锁无效
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
3.2.1.3 tryReleaseShared
//调用一次,减一次初始值
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
3.2.1.4 getCount
获得锁的数值。
3.2.2 CountDownLatch()构造方法
该构造函数可以构造一个用给定计数初始化的CountDownLatch,并且构造函数内完成了sync的初始化,并设置了状态数。
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
3.2.3 countDown()
作用:此函数将递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
由代码可见, countDown()主要的流程是tryReleaseShared->doReleaseShared.其中tryReleaseShared方法上文已经简单分析,tryReleaseShared方法每次调用都会讲state值减一,只有减到值为0时,开始执行doReleaseShared(),释放该线程;
3.2.4 await()
此函数将会使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
由代码可见,主要的方法调用链是tryAcquireShared->doAcquireSharedInterruptibly。doAcquireSharedInterruptibly和doAcquireShared方法体类似,本文不在讲述,重点讲述下逻辑。
tryAcquireShared小于0时,说明state还未被消费完,线程在代码边界处停留继续消费。
注意:CountDownLatch没有使用队列,仅用了自旋。
四、总结
4.1 区别
4.1.1 节点对象
在Node
类中通过nextWaiter
来标识共享模式(SHARED
)与独占模式(EXCLUSIVE
)下的节点。
共享模式的节点对象是
static final Node SHARED = new Node();
Node node = new Node(Thread.currentThread(), mode);
独占模式的节点对象是
static final Node EXCLUSIVE = null;
Node node = new Node(Thread.currentThread(), mode);
4.1.2 线程唤醒的时机
共享模式下,头节点获取共享锁后可以立即唤醒后继节点,而不用等待获取共享锁后释放再唤醒,唤醒后继线程有2处,一处是获取到共享锁后可以立即唤醒后续的线程,但是后续线程必须是共享模式的线程;第二处是在释放锁后唤醒后继线程,这边我认为释放锁后唤醒的后继线程可以包含独占模式,但是前提是所有的独占模式前面所有的共享模式锁都已经释放。