参考: Java并发之AQS详解
Lock
使用Lock
时要显式地获取和释放锁,虽然获取锁和释放锁相比 synchronized
要麻烦,但是麻烦意味着对于所得操控更加灵活,可以可中断获取锁、超时获取锁等。。
Lock
只是一个接口, 它定义了一些获取锁和释放锁的基本操作
方法 | 描述 |
---|---|
void lock() | 获取锁,获取后返回 |
void lockInterruptibly() | 获取锁,直到获取成功或者被中断 |
boolean tryLock() | 尝试获取锁,立即返回结果 |
boolean tryLock(long time, TimeUnit unit) | 如果在指定时间内获取到了锁并没有没中断过,则返回true |
void unlock() | 释放锁,释放或返回 |
Condition newCondition() | 获取“等待通知”组件 |
下面通过一个 Lock
的实现类 ReentrantLock
来分析具体方法的内部实现。
ReentrantLock
如图是 ReentrantLock
类的关系图。
我们看到 ReentrantLock
实现了 Lock
接口, 并且内部有一个 Sync
类, 而 Sync
类继承自 AbstractQueueSynchronizer
。
再进入 ReentrantLock
内部看看源码。
public void lock() {
sync.lock();
}
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
public void unlock() {
sync.release(1);
}
....
可以看出 ReentrantLock
类的锁操作方法都调用了 Sync
类的方法。 所以说,ReentrantLock
的锁操作都是由 继承了 AbstractQueueSynchronizer
的 Sync
类完成的。
AbstractQueueSynchronizer
队列同步器是用构建锁或者其他同步组件的基础框架,是实现锁的关键。在锁的实现中聚合同步器,利用同步器实现锁的语义。利用这种组合, 锁只需实现与使用者交互的接口, 而锁的实现交给内部的同步器去完成。
AbstractQueueSynchronizer 队列同步器
概览
图片来自 Java并发之AQS详解
同步器在内部使用了 private volatile int state;
变量表示同步状态, 通过内置的 FIFO 队列完成资源获取线程的排队工作。 同步器的设计是基于模板方法模式的,使用者只需继承 AbstractQueueSynchronizer
并重写相关方法,就能与自定义同步组件组合,完成自定义同步组件的实现(如:ReentrantLock
).
在同步器内部的等待队列中,只有头结点是获取了同步状态的,其他的结点要么在等待,要么正在前往等待途中。
自定义的同步器需要实现以下方法:
tryAcquire(int) //获取资源
tryRelease(int) //释放资源
tryAcquireShared(int) //获取共享资源
tryReleaseShared(int) //释放共享资源
isHeldExclusively() //返回是否独占资源
AbstractQueueSynchronizer
同时实现了一些模板方法供子类继承使用,模板方法分为三类: 独占式获取-释放、共享式获取释放 和 查询同步队列中的等待情况。主要说一下独占式获取-释放。
独占式获取-释放操作是下面两个方法:
void acquire(int arg)
boolean release(int arg)
独占式同步状态的获取 acquire()
acquire(int arg)
源码如下
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//acquireQueued 方法在自旋获取同步状态后 返回线程是否被中断过
//由于在获取同步状态过程中是不可被中断的, 因此需要在这里把中断状态补上
selfInterrupt();
}
方法中调用了 tryAcquire(arg)
方法完成同步状态的获取 , 如果获取成功直接返回。从前面知道, truAcquire()
是需要子类重写的方法,因此在 AQS 类中此方法只有一行抛出异常代码:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
主要看一下获取失败情况,获取失败后执行 addWaiter()
和 acquireQueued()
方法。addWaiter()
完成节点的入队操作, acquireQueued()
完成节点入队后自旋检查状态的操作。
addWaiter()
private Node addWaiter(Node mode) {
//完成队列节点构造
Node node = new Node(Thread.currentThread(), mode);
// 获取队尾节点, 尝试快速进入队列
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//快速进入队列失败后才转入enq()方法
enq(node);
return node;
}
//enq 方法中是一个自旋操作, 不断地通过 CAS 自旋操作尝试入队列
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;
//CAS 操作尝试入队
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued()
acquireQueued()
方法中也是一个自旋过程,是节点(线程)获取同步状态的核心函数
final boolean acquireQueued(final Node node, int arg) {
//记录获取同步状态是否失败
boolean failed = true;
try {
//记录是否被中断过
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor(); //首先获取当前节点(线程) 在队列中的前驱节点
//如果前驱节点是头结点,说明前驱节点已经获得同步状态, 因此需要 tryAcquire()
if (p == head && tryAcquire(arg)) {
//获取同步状态成功,此结点成为头结点, 并返回
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
/*
* 当前驱节点不是头结点,或者获取同步失败时要执行两个方法
* shouldParkAfterFailedAcquire 确定是否需要当前节点 "休息" 等待
* parkAndCheckInterrupt 如果确定需要等待,检查是否被中断
* */
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire(p, node)
次方法会在当前节点 node
获取同步状态失败时执行, 目的是判断线程是否需要阻塞(block
), 即是否需要执行后续 park
操作. 整个获取同步状态过程中的信号控制都发生在这个方法中。
节点(线程)的等待状态 waitStatus
有5种:
等待状态 | waitStatus 值 | 描述 |
---|---|---|
CANCELLED | 1 | 由于等待线程超时或者被中断需要取消等待状态, 取消后不可恢复,状态将不会变化 |
SIGNAL | -1 | 后继节点处于等待状态,而前驱节点一旦完成同步操作后会通知后继节点,当前驱节点是此状态时,后继节点可以放心 “休息” |
CONDITION | -2 | 节点在 Condition 上的等待队列中,不在这里的同步队列,当其他线程对 Condition 调用 signal() 后节点才会转到同步队列 |
PROPAGATE | -3 | 共享式同步状态获取将会无条件传播下去 |
INITIAL | 0 | 初始状态 |
JDK源码的注释已经很详细了:
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;
}
parkAndCheckInterrupt()
此方法功能很简单,就是让线程进入 WAITING 状态,返回返回中断状态
park与wait的作用类似,但是对中断状态的处理并不相同。如果当前线程不是中断的状态,park与wait的效果是一样的;如果一个线程是中断的状态,这时执行wait方法会报 java.lang.IllegalMonitorStateException
,而执行park时并不会报异常,而是直接返回。
只有以下四种情况中的一种发生时,park
方法才会返回。
- 与park对应的unpark执行或已经执行时。注意:已经执行是指unpark先执行,然后再执行的park。
- 线程被中断时。
- 如果参数中的time不是零,等待了指定的毫秒数时。
- 发生异常现象时。这些异常事先无法确定。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //尝试线程进入 WAITING 状态。
/*
* Thread.interrupted() 方法会清空中断标志位. 当park()直接返回说明被中断了,因此清空中断返回 true, 等下次循环时就能 park 了;
* 如果没有中断,那就 park 成功, 然后返回 false
*/
return Thread.interrupted();
}
cancelAcquire() 取消获取
此方法在获取同步状态失败时调用, 作用是将当前节点状态设置为 CANCELED
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
//跳过左右状态为 CANCELLED 的节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
//设置当前节点状态为 CANCELLED
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
//1. 如果当前节点为尾结点,则移除
//方法是 通过设置当前节点前驱为尾结点 并 设置前驱节点的后继为 null
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
//2. 如果不是尾结点 也不是头结点的后继节点 判断当前节点的前驱节点状态
//如果是 SIGNAL 或 者 设置状态为 SIGNAL 成功,则将前驱节点的后继设为当前节点的后继
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
//3. 如果当前节点是head的后继节点,则唤醒 当前节点的后继。
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
总结:
- 当前节点是tail;直接移除,即把前驱节点的后继更新为 null
- 当前节点不是tail,也不是head的后继节点(即队列的第一个节点,不包括head),两步操作: 1. 设置前驱节点状态为 SIGNAL 2,将前驱节点的后继设为当前节点的后继
- 当前节点不是tail, 是head的后继节点。unpark后继节点的线程,然后将next指向了自己。
注意上述操作只更新了涉及到的节点的 next ,而没有更新 prev, 这为后面的 unparkSuccessor
方法留了个坑。
参考: 深入理解AbstractQueuedSynchronizer(一)
小结
至此整个 acquire()
方法流程走完了,用一个流程图总结一下:
独占式释放release()
释放同步状态的过程比较简单,首先 tryRelease(int arg)
释放状态, 然后通知后继节点当前同步状态已释放
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
//如果当前节点状态小于0 则将状态置为0.因为当前节点已经完成同步后的操作
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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);
}
这里我们看到在找一个有效的等待节点过程是从后向前找的, 结合前面的 cancelAcquire
方法我们就能知道,在前面的 cancelAcquire
方法中更新了 不可用节点(即状态为 CANCELLED)的 next 而没有 更新 prev,因此如果这里从前向后找可用节点有可能陷入死循环。因为在前面的 cancelAcquire
方法中,不可用节点可能把它的 next 指向了自己。
总结
在获取同步状态时, 同步器维护了一个同步队列, 获取状态失败的线程都被加入此队列, 并在队列中进行自旋获取同步状态(前驱节点状态为 SIGNAL, 其他情况节点可能在阻塞)。当前驱节点为 head 头结点并成功获取了同步状态并释放后,节点才会获取到同步状态。这里当头节点释放同步状态时会调用 tryRelease()
然后唤醒后继节点。