概述
本文首先用导图总结一下上一篇文档写的内容,然后通过Mutex,ReentrantLock说明如何使用AQS,同时关注公平和非公平这个条件,最后关注一下可中断和条件这两个特性。
上一篇的总结:
Mutex&ReentrantLock&公平/非公平
在jdk8的current包中并没有找到Doug Lea的Mutex类,不过这个不影响,就当我们自己写的也一样。
class Mutex implements Lock, java.io.Serializable {
// 自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 判断是否锁定状态
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 尝试获取资源,立即返回。成功则返回true,否则false。
public boolean tryAcquire(int acquires) {
assert acquires == 1; // 这里限定只能为1个量
if (compareAndSetState(0, 1)) {//state为0才设置为1,不可重入!
setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源
return true;
}
return false;
}
// 尝试释放资源,立即返回。成功则为true,否则false。
protected boolean tryRelease(int releases) {
assert releases == 1; // 限定为1个量
if (getState() == 0)//既然来释放,那肯定就是已占有状态了。只是为了保险,多层判断!
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);//释放资源,放弃占有状态
return true;
}
}
// 真正同步类的实现都依赖继承于AQS的自定义同步器!
private final Sync sync = new Sync();
//lock<-->acquire。两者语义一样:获取资源,即便等待,直到成功才返回。
public void lock() {
sync.acquire(1);
}
//tryLock<-->tryAcquire。两者语义一样:尝试获取资源,要求立即返回。成功则为true,失败则为false。
public boolean tryLock() {
return sync.tryAcquire(1);
}
//unlock<-->release。两者语义一样:释放资源。
public void unlock() {
sync.release(1);
}
//锁是否占有状态
public boolean isLocked() {
return sync.isHeldExclusively();
}
}
读过AQS代码前面的注释,就会了解这种实现就是注释里面建议的AbstractQueuedSynchronizer使用方式。
- 通过内部类Sync继承AbstractQueuedSynchronizer,通过实现tryAcquire()/tryRelease()或者ryAcquireShared()/tryReleaseShared()方法实现独占/共享方式获取锁。
- 定义私有变量sync,提供外部接口lock()/unlock()。
接下来再来看一下ReentrantLock(主要关注它关于公平和非公平的特性)
ReentrantLock把所有Lock接口的操作都委派到一个Sync类上,该类继承了AbstractQueuedSynchronizer:
static abstract class Sync extends AbstractQueuedSynchronizer
Sync又有两个子类:
final static class NonfairSync extends Sync
final static class FairSync extends Sync
显然是为了支持公平锁和非公平锁而定义,默认情况下为非公平锁
非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
公平锁
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
共同的Sync
abstract static class Sync extends AbstractQueuedSynchronizer {
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
final boolean isLocked() {
return getState() != 0;
}
}
这里只贴出了部分代码。 小总结:
- 这里和Mutex的实现方式相似,都是通过内部的Sync类来实现。
- 公平锁和非公平锁的区别只是在获取锁的时候(在入AQS的CLH队列之前)代码有点区别,其它的都是一样的。一旦进入了队列,所有线程都是按照队列中先来后到的顺序请求锁。
- 非公平锁的代码中总是优先尝试当前是否有线程持有锁,一旦没有任何线程持有锁,那么非公平锁就霸道的尝试将锁“占为己有”。
ps:小插曲。这个时候如果有人再问Lock和Synchronized的区别,可以这么回答了:
AbstractQueuedSynchronizer通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过Lock-Free(CAS)操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。 synchronized的底层也是一个基于CAS操作的等待队列,但JVM实现的更精细,把等待队列分为ContentionList和EntryList,目的是为了降低线程的出列速度;当然也实现了偏向锁,从数据结构来说二者设计没有本质区别。但synchronized还实现了自旋锁,并针对不同的系统和硬件体系进行了优化,而Lock则完全依靠系统阻塞挂起等待线程。 当然Lock比synchronized更适合在应用层扩展,可以继承AbstractQueuedSynchronizer定义各种实现,比如实现读写锁(ReadWriteLock),公平或不公平锁;同时,Lock对应的Condition也比wait/notify要方便的多、灵活的多。
但是后续的问题,只能祝你好运了……(学海无涯啊)
可中断
前提是AQS的acquire是不响应中断的(关于Intereput的知识点,自行百度一下即可,需要知道线程在运行状态下是不响应中断的)。 可中断和超时的实现都是在AbstractQueuedSynchronizer,没有交给子类来实现。如果在获取一个通过网络交互实现的锁时,这个锁资源突然进行了销毁,那么使用acquireInterruptibly的获取方式就能够让该时刻尝试获取锁的线程提前返回。而同步器的这个特性被实现Lock接口中的lockInterruptibly方法。根据Lock的语义,在被中断时,lockInterruptibly将会抛出InterruptedException来告知使用者
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
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;
}
// 检测中断标志位
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
上述逻辑主要包括:
- 检测当前线程是否被中断; 判断当前线程的中断标志位,如果已经被中断了,那么直接抛出异常并将中断标志位设置为false。
- 尝试获取状态; 调用tryAcquire获取状态,如果顺利会获取成功并返回。
- 构造节点并加入sync队列; 获取状态失败后,将当前线程引用构造为节点并加入到sync队列中。退出队列的方式在没有中断的场景下和acquireQueued类似,当头结点是自己的前驱节点并且能够获取到状态时,即可以运行,当然要将本节点设置为头结点,表示正在运行。
- 中断检测。 在每次被唤醒时,进行中断检测,如果发现当前线程被中断,那么抛出InterruptedException并退出循环。
超时
针对超时控制这部分的实现,主要需要计算出睡眠的delta,也就是间隔值。间隔可以表示为nanosTimeout = 原有nanosTimeout – now(当前时间)+ lastTime(睡眠之前记录的时间)。如果nanosTimeout大于0,那么还需要使当前线程睡眠,反之则返回false。
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
long lastTime = System.nanoTime();
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;
}
if (nanosTimeout <= 0) return false; if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
long now = System.nanoTime();
//计算时间,当前时间减去睡眠之前的时间得到睡眠的时间,然后被
//原有超时时间减去,得到了还应该睡眠的时间
nanosTimeout -= now - lastTime;
lastTime = now;
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- 加入sync队列; 将当前线程构造成为节点Node加入到sync队列中。
- 条件满足直接返回; 退出条件判断,如果前驱节点是头结点并且成功获取到状态,那么设置自己为头结点并退出,返回true,也就是在指定的nanosTimeout之前获取了锁。
- 获取状态失败休眠一段时间; 通过LockSupport.unpark来指定当前线程休眠一段时间。
- 计算再次休眠的时间; 唤醒后的线程,计算仍需要休眠的时间,该时间表示为nanosTimeout = 原有nanosTimeout – now(当前时间)+ lastTime(睡眠之前记录的时间)。其中now – lastTime表示这次睡眠所持续的时间。
- 休眠时间的判定。 唤醒后的线程,计算仍需要休眠的时间,并无阻塞的尝试再获取状态,如果失败后查看其nanosTimeout是否大于0,如果小于0,那么返回完全超时,没有获取到锁。 如果nanosTimeout小于等于1000L纳秒,则进入快速的自旋过程。那么快速自旋会造成处理器资源紧张吗?结果是不会,经过测算,开销看起来很小,几乎微乎其微。Doug Lea应该测算了在线程调度器上的切换造成的额外开销,因此在短时1000纳秒内就让当前线程进入快速自旋状态,如果这时再休眠相反会让nanosTimeout的获取时间变得更加不精确。
条件中断
我们先来看一个例子:
//首先创建一个可重入锁,它本质是独占锁
private final ReentrantLock takeLock = new ReentrantLock();
//创建该锁上的条件队列
private final Condition notEmpty = takeLock.newCondition();
//使用过程
public E take() throws InterruptedException {
//首先进行加锁
takeLock.lockInterruptibly();
try {
//如果队列是空的,则进行等待
notEmpty.await();
//取元素的操作...
//如果有剩余,则唤醒等待元素的线程
notEmpty.signal();
} finally {
//释放锁
takeLock.unlock();
}
//取完元素以后唤醒等待放入元素的线程
}
上面的代码片段截取自LinkedBlockingQueue,是Java常用的阻塞队列之一。注意一点:条件队列是建立在锁基础上的,而且必须是独占锁
分析
等待条件的过程:
- 在操作条件队列之前首先需要成功获取独占锁。
- 成功获取独占锁以后,如果当前条件还不满足,则在当前锁的条件队列上挂起,与此同时释放掉当前获取的锁资源(如果不释放锁资源会发生什么?)
- 如果被唤醒,则检查是否可以获取独占锁,否则继续挂起。
条件满足后的唤醒过程(以唤醒一个节点为例,也可以唤醒多个):
- 把当前等待队列中的第一个有效节点(如果被取消就无效了)加入同步队列等待被前置节点唤醒,如果此时前置节点被取消,则直接唤醒该节点让它重新在同步队列里适当的尝试获取锁或者挂起。
这里要注意,整个AQS分为两个队列,一个同步队列,一个条件队列,如图
源码分析
执行过程:
以我们上面提到的例子来分析
//条件队列入口,参考上面的代码片段
public final void await() throws InterruptedException {
//如果当前线程被中断则直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//把当前节点加入条件队列
Node node = addConditionWaiter();
//释放掉已经获取的独占锁资源
int savedState = fullyRelease(node);
int interruptMode = 0;
//如果不在同步队列中则不断挂起
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
//中断处理,另一种跳出循环的方式
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//走到这里说明节点已经条件满足被加入到了同步队列中或者中断了
//这个方法很熟悉吧?就跟独占锁调用同样的获取锁方法,从这里可以看出条件队列只能用于独占锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//走到这里说明已经成功获取到了独占锁,接下来就做些收尾工作
//删除条件队列中被取消的节点
if (node.nextWaiter != null)
unlinkCancelledWaiters();
//根据不同模式处理中断
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
- 首先看下加入条件队列的代码
/1.与同步队列不同,条件队列头尾指针是firstWaiter跟lastWaiter
//2.条件队列是在获取锁之后,也就是临界区进行操作,因此很多地方不用考虑并发
private Node addConditionWaiter() {
Node t = lastWaiter;
//如果最后一个节点被取消,则删除队列中被取消的节点
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//创建一个类型为CONDITION的节点并加入队列,由于在临界区,所以这里不用并发控制
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
//删除取消节点的逻辑虽然长,但比较简单就是链表删除
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
- 把节点加入到条件队列中以后,接下来要做的就是释放锁资源
//入参就是新创建的节点,即当前节点
final int fullyRelease(Node node) {
boolean failed = true;
try {
//这里这个取值要注意,获取当前的state并释放,这从另一个角度说明必须是独占锁(可以考虑下这个逻辑放在共享锁下面会发生什么?)
int savedState = getState();
//跟独占锁释放锁资源一样,不赘述
if (release(savedState)) {
failed = false;
return savedState;
} else {
//如果这里释放失败,则抛出异常
throw new IllegalMonitorStateException();
}
} finally {
//如果释放锁失败,则把节点取消,由这里就能看出来上面添加节点的逻辑中只需要判断最后一个节点是否被取消就可以了
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
- 接下来就该挂起了(先忽略中断处理,单看挂起逻辑)
//如果不在同步队列就继续挂起(signal操作会把节点加入同步队列)
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
//中断处理后面再分析
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//判断节点是否在同步队列中
final boolean isOnSyncQueue(Node node) {
//快速判断1:节点状态或者节点没有前置节点
//注:同步队列是有头节点的,而条件队列没有
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
//快速判断2:next字段只有同步队列才会使用,条件队列中使用的是nextWaiter字段
if (node.next != null)
return true;
//上面如果无法判断则进入复杂判断
return findNodeFromTail(node);
}
//注意这里用的是tail,这是因为条件队列中的节点是被加入到同步队列尾部,这样查找更快
//从同步队列尾节点开始向前查找当前节点,如果找到则说明在,否则不在
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
- 如果被唤醒且已经被转移到了同步队列,则会执行与独占锁一样的方法acquireQueued()进行同步队列独占获取.最后我们来梳理一下里面的中断逻辑以及收尾工作的代码
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
//这里被唤醒可能是正常的signal操作也可能是中断
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//这里的判断逻辑是:
//1.如果现在不是中断的,即正常被signal唤醒则返回0
//2.如果节点由中断加入同步队列则返回THROW_IE,由signal加入同步队列则返回REINTERRUPT
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
//修改节点状态并加入同步队列
//该方法返回true表示节点由中断加入同步队列,返回false表示由signal加入同步队列
final boolean transferAfterCancelledWait(Node node) {
//这里设置节点状态为0,如果成功则加入同步队列
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
//与独占锁同样的加入队列逻辑,不赘述
enq(node);
return true;
}
//如果上面设置失败,说明节点已经被signal唤醒,由于signal操作会将节点加入同步队列,我们只需自旋等待即可
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
- 在把唤醒后的中断判断做好以后,看await()中最后一段逻辑
//在处理中断之前首先要做的是从同步队列中成功获取锁资源
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//由于当前节点可能是由于中断修改了节点状态,所以如果有后继节点则执行删除已取消节点的操作
//如果没有后继节点,根据上面的分析在后继节点加入的时候会进行删除
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
//根据中断时机选择抛出异常或者设置线程中断状态
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
//实现代码为:Thread.currentThread().interrupt();
selfInterrupt();
}
- 最后signal()方法相对容易一些,一起看源码分析下
//条件队列唤醒入口
public final void signal() {
//如果不是独占锁则抛出异常,再次说明条件队列只适用于独占锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//如果条件队列不为空,则进行唤醒操作
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
//该方法就是把一个有效节点从条件队列中删除并加入同步队列
//如果失败则会查找条件队列上等待的下一个节点直到队列为空
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&(first = firstWaiter) != null);
}
//将节点加入同步队列
final boolean transferForSignal(Node node) {
//修改节点状态,这里如果修改失败只有一种可能就是该节点被取消,具体看上面await过程分析
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//该方法很熟悉了,跟独占锁入队方法一样,不赘述
Node p = enq(node);
//注:这里的p节点是当前节点的前置节点
int ws = p.waitStatus;
//如果前置节点被取消或者修改状态失败则直接唤醒当前节点
//此时当前节点已经处于同步队列中,唤醒会进行锁获取或者正确的挂起操作
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
小总结
至此,AQS的关键特性在这里都分析完毕了,这里做个总结。
- 我们首先关注独占锁和共享锁的实现,其次关注它公平/非公平,超时,条件中断的特性。
- 整个AQS依靠一个state状态,两个队列(CLH和ConditionObject)来实现,队列中都是Node节点,节点中的状态:0,SINAL,CONDITION,- - --- CANCEL都在分析中出现过了,相信以后知道这些都是在什么时候用的了。
- AQS在许多设计思路都值得借鉴,比如典型的自旋等待、CLH队列。
参考
www.cnblogs.com/onlywujun/a… ifeve.com/introduce-a… www.cnblogs.com/lfls/p/7615…