> 上一篇分析了关于AQS独占锁的执行流程和源码。在AQS中不仅涉及了独占锁,还涉及了共享锁及带超时时间的共享锁、中断共享锁。本文就讲解上述锁的获取锁和释放锁的原理。
AQS获取共享锁acquireShared
共享锁,顾名思义就是多个线程可以共享同一把锁,在JUC下面如CountDownLatch就是基于共享锁实现的。
那么了解了共享锁是什么,那么先来看下它是如何获取锁的。
源码
共享锁获取锁和独占锁获取锁在AQS中的逻辑是基本一致的,流程图可以参考我上一篇中关于AQS独占锁加锁的流程图。接下来先看下执行代码:
acquireShared:
public final void acquireShared(int arg) {
// 获取锁方法由各自子类实现
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
下面的方法和独占锁一样,在上一篇中已经对代码有详细说明,唯一与独占锁处理逻辑不一样的地方在于setHeadAndPropagate方法,独占锁此处只是将获取到锁的节点设置为了头节点,但是共享锁不仅将获取到锁的节点设置为了头节点,还会唤醒下一个节点。那么我们重点看下setHeadAndPropagate这个方法。
doAcquireShared:
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) {
// 竞争锁,当返回值<0的时候,那么就是获取锁失败,反之则成功获取锁。
int r = tryAcquireShared(arg);
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);
}
}
setHeadAndPropagate:这个方法首先将获取到锁的节点设置为头节点,然后唤醒下一个节点。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 将当前获取到锁的节点设置为头节点
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
// 满足向下传播条件
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
// 获取到锁的节点的next节点是共享锁节点,那么唤醒后续节点
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
doReleaseShared:唤醒下一个节点,之后这个节点就有机会去竞争锁了。
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) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 释放h.next节点
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
实现原理
1、线程A请求锁,首先去获取锁。若成功获取到锁,直接返回,否则执行doAcquireShared。假设当前没有线程持有这把锁,且线程A成功获取到这把锁,运行线程A。
2、此时线程B请求加锁,同样也会尝试获取锁,那么正常此时获取锁失败(A线程持有了这把锁)。执行doAcquireShared。
3、doAcquireShared是一个死循环,在doAcquireShared中将请求B加入等待队列,在加入前,等待队列为空,加入后,等待队列如图1所示。
4、此时B线程会判断在等待队列中B线程的前继节点是不是head,如果是,则在此获取锁。目前的情况假设A还没有释放线程,B依然拿不到锁。那么进入到下一次循环,因为doAcquireShared是一个死循环,拿不到锁就一直阻塞在这个方法中。
5、此时假设A还未释放锁,那么当线程C请求过来的时候,同样会经历步骤2和3,然后依然会判断线程C对应的节点的前继节点是不是头结点,此时肯定不是头结点,C的前继节点是B节点,那么会执行shouldParkAfterFailedAcquire方法。
6、在shouldParkAfterFailedAcquire方法中,首先会判断C节点的前继节点(也就是B节点)的状态是不是SIGNAL(表示存在后继节点需要被唤醒),此时节点B的状态为0(初始化状态),当状态为0是,会将B节点的状态置为SIGNAL,并且返回false。
7、此时C线程请求返回到doAcquireShared方法,因为返回了false,所以根据doAcquireShared方法的逻辑,会进入下一次循环(因为是死循环)。此时C执行下一次循环后,又会执行到shouldParkAfterFailedAcquire方法(因为C节点的前继节点还不是head),但此时执行shouldParkAfterFailedAcquire方法会返回true(因为前继节点B的状态在上面已经设置为SIGNAL)了。返回true后,在doAcquireShared方法中会执行parkAndCheckInterrupt方法,阻塞当前线程,也就是线程C被阻塞挂起了,等待其他线程来解阻塞。
综上,此时线程A持有锁,线程B在自旋,状态为SIGNAL,并且一直尝试获取锁,线程C阻塞挂起,状态为0。
AQS释放共享锁
AQS释放共享锁和上一篇讲的独占锁处理是基本一致的,都是唤醒后续节点。
1、假设此时A线程执行结束,释放锁。当A释放锁成功后,会执行doReleaseShared方法。
2 、在doReleaseShared中,主要是释放下一个节点,让它有机会去竞争锁,这里也就是唤醒B线程。
AQS可打断共享锁
acquireSharedInterruptibly:这个方法与共享锁获取锁逻辑基本一致,唯一不同的是在尝试获取锁之前,会判断当前线程的打断状态,如果当前线程线程被打断,那么直接抛出异常。
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 判断当前线程是否被打断,如果打断,那么直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
释放锁的流程和共享锁是一致的。
AQS超时模式独占锁
在获取锁的时候可以传入一个超时时间,当自旋等待的时间超过传入的等待时间时,那么返回获取锁失败,然后直接取消该线程所属节点(也就是将在等待队列中的该节点的状态设置为canceled)。
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 会判断等待是否超时,如果超时就直接返回,不会在这里自旋等待。
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
// 如果获取锁失败,可能是等待超时,那么直接将该节点设置为取消状态。
if (failed)
cancelAcquire(node);
}
}
重点看下上面带注释的两行代码,分别为判断等待时间是否超过传入的等待时间,也就是等待是否超时,如果等待超时,那么直接返回获取锁失败,并取消该节点。其余的处理和独占锁获取锁流程是完全一致的。
条件等待队列
我们先看下面的代码实例:
static ReentrantLock reentrantLock = new ReentrantLock();
static Condition condition = reentrantLock.newCondition();
static int a = 0;
public static void add() {
reentrantLock.lock();
while (a == 0) {
try {
a++;
System.out.println("add, a="+a);
condition.signal();
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
reentrantLock.unlock();
}
public static void sub() {
reentrantLock.lock();
while (a == 1) {
try {
a--;
System.out.println("sub, a="+a);
condition.signal();
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
reentrantLock.unlock();
}
public static void main(String[] args) throws Exception{
new Thread(new Runnable() {
@Override
public void run() {
add();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
sub();
}
}).start();
}
上面的代码很简单,是个简答的生产者消费者模型。其中ReentrantLock是独占锁,这个我们后面会有单独的文章来讲解。下面想说的是reentrantLock.newCondition(),那么Condition是什么呢?下面我们就来说一下。
Condition是AQS中的另一个队列,之前我们说过的队列是AQS等待队列,现在我们要说的也是AQS类中的一个队列,叫条件等待队列。首先当前线程要先获取到锁,当满足指定条件(程序员自己指定)时,可通过signal来唤醒其他线程来获取锁,然后通过await方法将当前线程阻塞,这个就是条件队列的作用,也就是可以切换不同线程来获取同一把锁。在AQS的源码中定义如下:
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/** First node of condition queue. */
// 头节点
private transient Node firstWaiter;
/** Last node of condition queue. */
// 尾节点
private transient Node lastWaiter;
/**
* Creates a new {@code ConditionObject} instance.
*/
public ConditionObject() { }
// Internal methods
/**
* Adds a new waiter to wait queue.
* @return its new wait node
*/
// 向队列中加入节点,状态为CONDITION
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;
}
/**
* Removes and transfers nodes until hit non-cancelled one or
* null. Split out from signal in part to encourage compilers
* to inline the case of no waiters.
* @param first (non-null) the first node on condition queue
*/
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
/**
* Removes and transfers all nodes.
* @param first (non-null) the first node on condition queue
*/
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
/**
* Moves the longest-waiting thread, if one exists, from the
* wait queue for this condition to the wait queue for the
* owning lock.
*
* @throws IllegalMonitorStateException if {@link #isHeldExclusively}
* returns {@code false}
*/
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
... ...
}
上面代码就是在AQS源码中定义的条件等待队列,包含了头节点和尾节点两个Node,这里其实就是一个双向链表。至于signal和await方法的原理,我将在后面的文章中专门来详细讲解,这里只需知道在AQS中不仅定义了一个等待队列,还定义了一个条件等待队列来供子类实现更多的功能(唤醒其他线程,阻塞当前线程)。
最后
本文包括上一篇AQS文章基本把AQS的总体结构及源码都介绍了一下,大家应该对AQS有了一个总体印象。接下来的本系列文章将分析JUC包下面的具体实现类,如ReentrantLock、CountDownLatch、CyclicBarrier、Semaphore等。