上一节 Java JUC 1 基础知识
下一节 Java JUC 3 -AQS同步组件
AQS
-
理解AQS
AbstractQueuedSynchronizer,它提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等并发类均是基于AQS来实现的,具体用法是通过继承AQS实现其模板方法,然后将子类作为同步组件的内部类。 -
AQS基本框架如下图所示:
AQS维护了一个volatile的共享变量state和一个FIFO线程等待队列。当state>0时表示已经获取了锁,当state = 0时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作;FIFO同步队列用来完成资源获取线程的排队工作,如果当前线程获取锁失败时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时阻塞当前线程,当同步状态释放时,会把节点中的线程唤醒,使其再次尝试获取同步状态。 -
资源的共享方式分为2种
-
独占式(Exclusive)
只有单个线程能够成功获取资源并执行,如ReentrantLock -
共享式(Shared)
多个线程可成功获取资源并执行,如Semaphore/CountDownLatch等。 -
AQS将大部分的同步逻辑均已经实现好,继承的自定义同步器只需要实现state的获取(acquire)和释放(release)的逻辑代码就可以,主要包括下面方法:
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
AQS需要子类复写锁的获取与释放,因为一般自定义同步器要么是独占方法,要么是共享方法,只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。AQS也支持子类同时实现独占和共享两种模式,如ReentrantReadWriteLock。
-
-
CLH队列
AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败,AQS则会将当前线程包装(Node)加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。-
CLH锁的基本原理:
- 使用FIFO队列保证公平性
- 有当前节点和前置节点,当前节点不断自旋,监听前置节点的状态(isLocked)(保证了FIFO)
- 一系列的前置节点和当前节点构成队列
- 当前节点运行完成后,更改自己的状态,那监听当前节点状态的线程就会结束自旋
-
Node节点:
在CLH同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next)
同步队列中被阻塞的线程的等待状态包含有四个常量值- CANCELLED 同步队列中当前节点的线程等待超时或被中断,需要从同步队列中取消等待。
- SIGNAL 当前节点释放同步状态或被取消后,通知后继节点的线程运行。
- CONDITION 当前节点在 Condition 上等待,当其他线程对 Condition 调用了 signal() 方法后,该节点将添加到同步队列中。
- PROPAGATE 该状态存在共享模式的首节点中,当前节点唤醒后将传播唤醒其他节点。
-
AQS对CLH的增强和进化
AQS基于CLH锁的访问前置节点信息的原理实现,并添加了一些功能:- 支持阻塞而不是一直自旋,竞争激烈时,阻塞性能更好
- 支持可重入
- 支持取消节点
- 支持中断
- 支持独占(互斥)和共享两种模式
- 支持Condition Condition替代对象监听器(Monitor)用来等待,唤醒线程,用于线程间的协作
-
-
等待队列
AQS中包含一个内部类ConditionObject,该类实现了Condition的接口。一个Condition对象包含一个等待队列,同时Condition对象可以实现等待/通知功能。
- Condition.await():如果当前线程调用 Condition.await() 时,同步队列中的首节点,也就是当前线程所创建的节点,会加入到等待队列中的尾部,释放锁并且唤醒同步队列的后继节点,当前线程也就进入等待状态。
public final void await() throws InterruptedException {
// 如果线程已中断,则抛出中断异常
if (Thread.interrupted())
throw new InterruptedException();
// 添加节点到等待队列的最后
Node node = addConditionWaiter();
// 修改 state 来达到释放同步状态,避免死锁
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);
}
- Condition.signal() :当调用Condition.signal()方法时,会先将等待队列中首节点转移到同步队列尾部,然后唤醒该同步队列中的线程,该线程从Condition.await()中自旋退出,接着在同步器的acquireQueued()中自旋获取同步状态。
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) {
// 验证节点是否被取消
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 转移节点至同步队列
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
独占式
1. 独占式同步状态
首先尝试获取锁,如果获取锁失败,则调用addWaiter将当前线程加入到CLH同步队列尾部,并且开始自旋,直到获取锁为止。
public final void acquire(int arg) {
// 去尝试获取锁->获取锁失败,则调用addWaiter将当前线程加入到CLH同步队列尾部,
// 并且开始自旋,直到获取锁为止
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
addWaiter会尝试快速添加到末尾,如果失败,使用cas再次尝试添加,还是失败,则调用enq死磕
private Node addWaiter(Node mode) {
//新建Node
Node node = new Node(Thread.currentThread(), mode);
// 1快速尝试添加尾节点
Node pred = tail;
if (pred != null) {
node.prev = pred;
// 2CAS设置尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 3多次尝试
enq(node);
return node;
}
acquireQueued 当前线程(Node)进入同步队列后,就会进入自旋,监测上一个节点,如果上一个节点是头节点,则尝试获取锁,如果获取锁成功,将自己设为头节点。否则调用shouldParkAfterFailedAcquire()检测自己是否需要立即阻塞,如果是就调用parkAndCheckInterrupt()阻塞。
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);
}
}
当前线程是否需要被阻塞,具体规则如下:
1. 如果当前线程的前驱节点状态为SINNAL,则表明当前线程需要被阻塞,调用unpark()方法唤醒,直接返回true,当前线程阻塞
2. 如果当前线程的前驱节点状态为CANCELLED(ws > 0),则表明该线程的前驱节点已经等待超时或者被中断了,则需要从CLH队列中将该前驱节点删除掉,直到回溯到前驱节点状态 <= 0 ,返回false
3. 如果前驱节点非SINNAL,非CANCELLED,则通过CAS的方式将其前驱节点设置为SINNAL,返回false
总结:只有前驱节点是SINNAL时需要,否则都不需要
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// //前驱节点
int ws = pred.waitStatus;
// 状态为signal,表示线程处于等待状态,直接返回true,需要等待
if (ws == Node.SIGNAL)
return true;
//前驱节点状态 > 0 ,则为Cancelled,表明该节点已经超时或者被中断了,需要从同步队列中取消
if (ws > 0) {
do {
// 移除
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//前驱节点状态为不是Condition、propagate,则通过CAS的方式将其前驱节点设置为SINNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
流程如下
2. 独占式同步状态释放
先调用自定义同步器自定义的tryRelease(int arg)方法来释放同步状态,释放成功后,会调用unparkSuccessor(Node node)方法唤醒后继节点(如何唤醒LZ后面介绍)。
public final boolean release(int arg) {
// 尝试释放锁
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);// 唤醒后继节点
return true;
}
return false;
}
共享式
共享式与独占式的最主要区别在于同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。
1. 共享式同步状态
首先是调用tryAcquireShared(int arg)方法尝试获取同步状态,如果获取失败则调用doAcquireShared(int arg)自旋方式获取同步状态,共享式获取同步状态的标志是返回 >= 0 的值表示获取成功。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
2. 享式同步状态释放
因为可能会存在多个线程同时进行释放同步状态资源,所以需要确保同步状态安全地成功释放,一般都是通过CAS和循环来完成的。
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;
}
}