若有不正之处请多多谅解,并欢迎批评指正。
请尊重作者劳动成果,转载请标明原文链接:
http://www.cnblogs.com/go2sea/p/5618628.html
AbstractQueuedSynchronizer(AQS)是一个同步器框架,在实现锁的时候,一般会实现一个继承自AQS的内部类sync,作为我们的自定义同步器。AQS内部维护了一个state成员和一个队列。其中state标识了共享资源的状态,队列则记录了等待资源的线程。以下这五个方法,在AQS中实现为直接抛出异常,这是我们自定义同步器需要重写的方法:
①isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
②tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败返回false。
③tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败返回false。
④tryAcquireShared(int):共享方式。尝试获取资源。成功返回true,失败返回false。
⑤tryReleaseShared(int):共享方式。尝试释放资源,成功则返回true,失败返回false。
其中isHeldExclusively需要在使用Condition时重写,他在AQS中的调用全部发生在其内部类ConditionObject的方法中。②③和④⑤分别对应了AQS定义的两种资源共享的方式:Exclusive&share,例如ReentrantLock就是一种独占锁,CountDownLatch和Semaphore是共享锁。与CountDownLatch有一定相似性的CyclicBarrier并没有自己的共享同步器,而是使用Lock和Condition来实现的(关于CyclicBarrier的详解可以参考本人的另一篇博文http://www.cnblogs.com/go2sea/p/5615531.html)。
下面是一个简单的独占锁的实现,它是不可重入的。它重写了AQS的tryAcquire方法和tryRelease方法:
1 import java.io.Serializable;
2 import java.util.concurrent.TimeUnit;
3 import java.util.concurrent.locks.AbstractQueuedSynchronizer;
4 import java.util.concurrent.locks.Condition;
5 import java.util.concurrent.locks.Lock;
6
7 class Mutex implements Lock, Serializable {
8 //自定义同步器,继承自AQS
9 private static class Sync extends AbstractQueuedSynchronizer {
10 //试图获取锁,当state为0时能成功获取,
11 public boolean tryAcquire(int acquires) {
12 assert acquires == 1; //这是一个对于state进行操作的量,含义自定义
13 if (compareAndSetState(0, 1)) { //注意:这是一个原子操作
14 setExclusiveOwnerThread(Thread.currentThread());
15 return true;
16 }
17 return false;
18 }
19 //释放锁,此时state应为1,Mutex处于被独占状态
20 protected boolean tryRelease(int releases) {
21 assert releases == 1; // Otherwise unused
22 if (getState() == 0) throw new IllegalMonitorStateException();
23 setExclusiveOwnerThread(null);
24 setState(0);
25 return true;
26 }
27 //返回一个Condition
28 Condition newCondition() { return new ConditionObject(); }
29 }
30
31 private final Sync sync = new Sync();
32
33 public void lock() { sync.acquire(1); }
34 public boolean tryLock() { return sync.tryAcquire(1); }
35 public void unlock() { sync.release(1); }
36 public Condition newCondition() { return sync.newCondition(); }
37 public void lockInterruptibly() throws InterruptedException {
38 sync.acquireInterruptibly(1);
39 }
40 public boolean tryLock(long timeout, TimeUnit unit)
41 throws InterruptedException {
42 return sync.tryAcquireNanos(1, unit.toNanos(timeout));
43 }
44 }
我们可以看到,利用AQS实现一个简单的自定义锁看上去并不复杂,让我们以此为例,来学习一下AQS的内部原理吧。
一、acquire 获取锁
我们先来看一下Mutex重写的tryAcquire方法:
//试图获取锁,当state为0时能成功获取,
public boolean tryAcquire(int acquires) {
assert acquires == 1; //这是一个对于state进行操作的量,含义自定义
if (compareAndSetState(0, 1)) { //注意:这是一个原子操作
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
注意:当我们初始化一个Sync的时候,如果没有指定state的初值(无参数),那么state的默认初值是0。可以看到,方法开头首先有一个断言acquires==1,参数acquires代表要在state上做的改变的量(减去或增加),在Mutex中,我们定义state只有两个状态:0或1,0代表共享资源可以被获取,1表示共享资源正在被占用,因此Mutex是不可重入的。实际上,自定义同步器通过重写tryAcquire和tryRelease来定义state代表的意义和资源的共享方式,这是同步器的主要任务。Mutex的tryAcquire使用一个原子操作compareAndSetState来试图获取资源,这个原子操作由上层的AQS提供,如果成功,将当前线程设置为独占线程并返回true。
Mutex的lock方法调用了sync的acquire方法,acquire方法实现为:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
它首先调用tryAquire去获取共享资源,如果失败,调用addWaiter将当前线程放入等待队列,返回持有当前线程的Node对象,然后调用acquireQueued方法来监视等待队列并获取资源。acquireQueued方法会阻塞,直到成功获取。注意,acquire方法不能及时响应中断,只能在成功获取锁之后,再来处理。中断当前线程的操作跑出的异常在acquireQueued方法中被捕获,外部调用者没能看到这个异常,因此调用selfInterrupt来重置中断标识。
我们需要详细了解addWaiter方法和acquireQueued方法,之后再来回顾acquire的过程,才能对整个获取锁的流程有比较详细的了解。
我们先来看addWaiter方法:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
addWaiter首先将当前线程包装在一个Node对象node中,然后获取了一下队列的尾节点,如果队列不为空(tail不为null)的话,调用一个CAS函数试图将node放入等待队列的尾部,注意,此时可能发生竞争,如果有另外一个线程在两个if之间抢先更新的队列的尾节点,CAS操作将会失败,这时会调用enq方法,继续试图将node放入队列:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
enq方法会循环检测队列,如果队列为空,则调用CAS函数初始化队列(此时node==head==tail),否则调用CAS函数将node放入队列尾。注意,这两个CAS是由AQS提供的原子操作。如果CAS失败,enq会继续循环检测,直到成功将node入列。enq方法的这种方式有一个专用的名词:CAS自旋,这种方式在AQS中有多处应用。这里有一个隐含的知识点,即tail是一个volatile成员,确保某个线程更新队列后对其他线程的可见性。
注意:队列为空的时候,第一个线程进入队列的情况有点tricky:第一个发现队列为空并初始化队列(head节点)的线程不一定优先拿到资源。head节点被初始化后,当前线程需要下一次旋转才有机会进入队列,在这期间,完全有可能半路杀出程咬金,将当前线程与它初始化出的head节点无情分开。我们来总结一下,当队列只有一个节点时(head=tail),有两种情况:第一种是这个队列刚刚被初始化,head并没有持有任何线程对象。这个状态不会持续太久,初始化队列的线程有很大机会在下次自旋时把自己接到队尾。第二种情况是,所有等待线程都已经获得资源并继续执行下去了,队列仅有的节点是最后一个获取共享资源的线程,等到下一个线程到达等待队列并将它踢出队列之后,它才有机会被回收。
enq执行完毕,我们已经成功把当前线程放入等待队列,接下来的任务就是监视队列,等待获取资源。这个过程由acquireQueued方法实现:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
acquireQueued方法是一个很重要的方法,在分析这个方法之前,我们先来说一下AQS中的那个等待队列。这个队列实际上是一个CLH队列,它保证了竞争资源的线程按到达顺序来获取资源,避免了饥饿的发生。CLH队列的工作过程,就是acquireQueued方法的工作过程。很明显,这又是一个自旋。首先,我们调用predecessor方法获取当前线程的前驱节点,如果这个前驱是head节点,就紧接着调用tryAcquire去获取共享资源,当然这是有可能失败的,因为head节点可能刚刚“上位”,还没有释放资源。如果很幸运,我们拿到了资源,就调用setHead将node设置为队列的头结点,setHead方法同时会将node的prev置为null,紧接着将原先head的next也置为null,显然这是为了让其后续被回收。注意:acquireQueued方法在自旋过程中是不可被中断的,当然它会检测到中断(在parkAndCheckInterrupt方法中检测中断标志),但并不会因此结束自旋,只能在获得资源退出方法后,反馈给上层的方法:我刚刚被中断了。还记得acquire方法中的selfInterrupt的调用吗,就是为了“补上”这里没有响应的中断。
好,我们继续往下。获取资源失败后(原因有二,head与我之间还有等待线程或者head节点的线程正在使用资源),调用shouldParkAfterFailedAcquire方法检测是否该去“休息”下,毕竟一直自旋很累嘛。如果可以休息就调用parkAndCheckInterrupt放心去休息。我们先来看一下shuldParkAfterFailedAcquire:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
我们首先了解一下waitStatus。Node对象维护了一个int成员waitStatus,他的可能取值如下:
static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3;
下面解释一下每个值的含义
CANCELLED:因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态。处于这种状态的结点会被踢出队列,被GC回收;
SIGNAL:表示这个结点的继任结点被阻塞了,到时需要通知它;
CONDITION:表示这个结点在条件队列中,因为等待某个条件而被阻塞;
PROPAGATE:使用在共享模式头结点有可能处于这种状态,表示锁的下一次获取可以无条件传播;
0:None of the above,新结点会处于这种状态。
在我们的Mutex的例子中,节点的waitStatus只可能有CANCELLED、SIGNAL和0三中状态(事实上,独占模式下所有不使用Condition的同步器都是这样)。
我们继续来分析shouldParkAfterFailedAcquire方法:
首先检测下node的前驱节点pred,如果pred状态已经被置为SIGNAL,直接返回true。否则,从node的前驱继续往前找,直到找到一个waitStatus小于等于0的节点,设置该点为node的前驱(注意:此时node与这个节点之间的节点从等待队列中被“摘下”,等待被回收了)并返回false。返回之后,上层的acquireQueued方法继续自旋,再次进入shouldParkAfterFailedAcquire方法之后,如果发现node前驱不是取消状态且waitStatus不等于SIGNAL,调用CAS函数进行注册。注意:这个操作可能失败,因此不能直接返回true,而是返回false由上层的自旋再次调用shouldParkAfterFailedAcquire直到确认注册成功。
历尽曲折,我们终于可以安心休息了:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
parkAndCheckInterrupt方法十分简单,他调用LockSupport的静态方法park阻塞当前线程,直到被中断,这次中断会被acquireQueued记录,但不会立即响应,直到自旋完成。注意:返回操作中的interrupted方法会将中断标志复位,因此我们在上层需要将这个中断“补上”,再一次:还记得大明湖边的selfInterrupt吗?
二、release 释放锁
我们先来看一下Mutex中重写的tryRelease方法:
//释放锁,此时state应为1,Mutex处于被独占状态
protected boolean tryRelease(int releases) {
assert releases == 1; // Otherwise unused
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
逻辑比较简单,首先将独占线程置为null,紧接着将state设置为0,这里不会发生资源竞争,因此不需要用CAS去设置state值,直接置0即可。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
好,我们开始分析release方法。首先调用tryRelease试图释放共享资源,紧接着检测自己的waitStatus是否为SIGNAL,如果是的话,调用unparkSuccessor唤醒队列中的下一个线程。独占模式下,waitStatus!=0与waitStatus==-1等价(这里waitStatus不会为CANCELLED,因为已经获取资源了)。如果不为SIGNAL,说明如果有下个等待线程,它正在自旋。所以直接返回true即可。我们来看下unparkSuccessor方法:
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
unparkSuccessor方法也会在共享模式的工作流程中被调用,因此方法开始做的判断是有必要的。对于独占模式而言,ws应该都是0。然后找到下一个需要被唤醒的线程并调用LockSupport的静态方法unpark唤醒等待线程。
至此,我们比较详细地了解了acquire&release的工作流程。
三、acquireShared 获取锁
下面,我们来学习下共享模式下的获取&释放锁的工作流程。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
acquireShared方法首先调用tryAcquireShared试图获取共享资源。tryAcquireShared的返回值表示剩余资源个数,负值表示获取失败,0表示获取成功但已无剩余资源。如果获取失败,调用doAcquireShared方法完成独占模式下类似的操作,后面我们会详细分析。注意,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) {
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);
}
}
doAcquireShared方法与acquireQueued方法相似,不同的地方在于,共享模式下成功获取资源并将head指向自己之后,要检查并试图唤醒之后的等待线程。因为共享资源可能剩余,可以被后面的等待线程获取。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
setHeadAndPropagate中有一个长长的if,来判断是否应该去试图唤醒后面的线程。其中h==null的判断笔者始终不能理解,因为查看代码发现,之后队列尚未初始化的时候为空,后续都不可能为空了。关于这点希望各位看官不吝指教。其他情况,propagate大于0,表示尚有资源可被获取,显然应该继续判断;而当h.waitStatus小于0时,它有两种取值可能,SIGNAL和PROPAGATE,我们将在后面看到,这两种情况都是应该继续判断。后续是对node的后继进行的判断,注意,node此时可能已经不是head节点了,因为这是共享模式,所以可能有一个node的后继成功获取资源后,把自己设为head,将node踢出了队列。这种情况下node的后继s是可能为null的,但貌似这种情况doReleaseShared的调用没有意义。s.isShared的判断主要是考虑到读写锁的情况,在读写锁的使用过程中,申请写锁(独占模式)和申请读锁(共享模式)的线程可能同时存在,这个判断发现后即线程是共享模式的时候,调用soReleaseShared方法唤醒他。
但总之,我们十分保守谨慎地调用了doReleaseShared方法试图唤醒后继线程:
private void doReleaseShared() {
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
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
又是一个自旋。我们首先获取head节点h,然后检查它的waitStatus是否为SIGNAL,如果是的话,调用CAS将h的waitStatus设置为0,并调用unparkSuccessor唤醒下一个等待线程。注意,这里调用CAS方法而不是直接赋值,是因为在共享模式下,这里可能发生竞争。doReleaseShared方法可能由head节点在使用完共享资源后主动调用(后续在releaseShared方法中可以看到),也可能由刚刚“上位”的等待线程调用,在上位之后,原来的head线程已被踢出队列。
因此,doReleaseShared方法的执行情况变得比较复杂,需要细致分析。
第一种情况,只有刚刚释放资源的head线程调用,这时候没有竞争,waitStatus是SIGNAL,就去唤醒下个线程,是0,就重置为PROPAGATE。
第二种情况,刚刚释放完资源的旧head,和刚刚上位的新head同时调用doReleaseShared方法,这时候最新的head获取的都是自己,若干被踢出的旧head获取的可能是旧head,也可能是新head,这些被踢出的旧head线程也在根据自己获取的head(不管新旧)的状态进行CAS操作和unparkSuccessor操作,幸运的是(必须幸运啊。。),这些操作不会造成错误,只是多了一些唤醒而已(这些唤醒可能导致一个线程获得资源,也可能是一个“虚晃”)。
我们可以发现,不管head引用怎样更迭,最终新head的waitStatus都会被顺利处理。注意,可能有多个旧head同时参与这个过程,都不影响正确性。
我们注意到,一个新head,在他刚上位的时候有机会调用一次setHeadAndPropagate进而调用doReleaseShared,在他释放资源之后,又一次调用doReleaseShared(这次是必然的)。第一次调用时,不管新head的waitStatus是0还是SIGNAL,最终状态都被PROPAGATE(当然,被踢出队列的head可能还没来得及设置成PROPAGATE,但新上位的head最终会被设置),这也符合PROPAGATE的语义:使用在共享模式头结点有可能处于这种状态,表示锁的下一次获取可以无条件传播。
还有一个问题,它是由SIGNAL-->0-->PROPAGATE变化而来的,为什么不是SIGNAL-->PROPAGA这样直接变化呢?原因是unparkSuccessor方法会试图将当前node的waitStatus复位成0,如果我们直接SIGNAL-->PROPAGA后,那么又被复位成0,还需要一次CAS操作置为PROPAGATE。
四、releaseShared 释放锁
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
我们可以看到,调用tryReleaseShared成功释放共享资源之后,最终要再次调用doReleaseShared试图唤醒后面的等待线程。
五、ConditionObject
关于Condition的内容请看笔者的另一篇博文Condition源码学习笔记。
至此,我们对独占模式和共享模式下、不响应中断的、没有等待时间参数的获取资源和释放资源的流程有了初步了解。这时去看JUC包中的锁的源码,相信会有更深的理解。
来源:https://www.cnblogs.com/go2sea/p/5618628.html