最后
金三银四马上就到了,希望大家能好好学习一下这些技术点
学习视频:
大厂面试真题:
上一节的队列其实是AQS的同步队列,这一节的队列是条件队列,队列的管理除了有同步队列,还有条件队列。AQS只有一个同步队列,但是可以有多个条件队列。AQS框架提供了一个ConditionObject类,给维护独占同步的类以及实现Lock接口的类使用。
ConditionObject类实现了Condition接口,Condition接口提供了类似Object管程式的方法,如await、signal和signalAll操作,还扩展了带有超时、检测和监控的方法。ConditionObject类有效地将条件与其它同步操作结合到了一起。该类只支持Java风格的管程访问规则,这些规则中,当且仅当当前线程持有锁且要操作的条件(condition)属于该锁时,条件操作才是合法的。这样,一个ConditionObject关联到一个ReentrantLock上就表现的跟内置的管程(通过Object.wait等)一样了。两者的不同仅仅在于方法的名称、额外的功能以及用户可以为每个锁声明多个条件。
ConditionObject类和AQS共用了内部节点,有自己单独的条件队列。signal操作是通过将节点从条件队列转移到同步队列中来实现的,没有必要在需要唤醒的线程重新获取到锁之前将其唤醒。signal操作大致示意图如下:
await操作就是当前线程节点从同步队列进入条件队列进行等待,大致示意图如下:
实现这些操作主要复杂在,因超时或Thread.interrupt导致取消了条件等待时,该如何处理。await和signal几乎同时发生就会有竞态问题,最终的结果遵照内置管程相关的规范。JSR133修订以后,就要求如果中断发生在signal操作之前,await方法必须在重新获取到锁后,抛出InterruptedException。但是,如果中断发生在signal后,await必须返回且不抛异常,同时设置线程的中断状态。
2.2 方法结构
如果我们理解了上一节的设计思路,我们大致就能知道AQS的主要数据结构了。
组件 | 数据结构 |
同步状态 | volatile int state |
阻塞 | LockSupport类 |
队列 | Node节点 |
条件队列 | ConditionObject |
进而再来看下AQS的主要方法及其作用。
属性、方法 | 描述、作用 |
int getState() | 获取当前同步状态 |
void setState(int newState) | 设置当前同步状态 |
boolean compareAndSetState(int expect, int update) | 通过CAS设置当前状态,此方法保证状态设置的原子性 |
boolean tryAcquire(int arg) | 钩子方法,独占式获取同步状态,AQS没有具体实现,具体实现都在子类中,实现此方法需要查询当前同步状态并判断同步状态是否符合预期,然后再CAS设置同步状态 |
boolean tryRelease(int arg) | 钩子方法,独占式释放同步状态,AQS没有具体实现,具体实现都在子类中,等待获取同步状态的线程将有机会获取同步状态 |
int tryAcquireShared(int arg) | 钩子方法,共享式获取同步状态,AQS没有具体实现,具体实现都在子类中,返回大于等于0的值表示获取成功,反之失败 |
boolean tryReleaseShared(int arg) | 钩子方法,共享式释放同步状态,AQS没有具体实现,具体实现都在子类中 |
boolean isHeldExclusively() | 钩子方法,AQS没有具体实现,具体实现都在子类中,当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占 |
void acquire(int arg) | 模板方法,独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则会进入同步队列等待,此方法会调用子类重写的tryAcquire方法 |
void acquireInterruptibly(int arg) | 模板方法,与acquire相同,但是此方法可以响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,此方法会抛出InterruptedException并返回 |
boolean tryAcquireNanos(int arg, long nanosTimeout) | 模板方法,在acquireInterruptibly基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,则会返回false,如果获取到了则会返回true |
boolean release(int arg) | 模板方法,独占式的释放同步状态,该方法会在释放同步状态后,将同步队列中的第一个节点包含的线程唤醒 |
void acquireShared(int arg) | 模板方法,共享式的获取同步状态,如果当前系统未获取到同步状态,将会进入同步队列等待,与acquire的主要区别在于同一时刻可以有多个线程获取到同步状态 |
void acquireSharedInterruptibly(int arg) | 模板方法,与acquireShared一致,但是可以响应中断 |
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) | 模板方法,在acquireSharedInterruptibly基础上增加了超时限制 |
boolean releaseShared(int arg) | 模板方法,共享式的释放同步状态 |
Collection<Thread> getQueuedThreads() | 模板方法,获取等待在同步队列上的线程集合 |
Node int waitStatus | 等待状态 1、 CANCELLED,值为1,在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待,节点进入该状态后将不会变化; 2、 SIGNAL,值为-1,后续节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后续节点,使后续节点的线程得以运行; 3、 CONDITION,值为-2,节点在条件队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从条件队列中转移到同步队列中,加入到对同步状态的获取中; 4、 PROPAGATE,值为-3,表示下一次共享式同步状态获取将会无条件地传播下去 |
Node prev | 前驱节点,当节点加入同步队列时被设置 |
Node next | 后续节点 |
Thread thread | 获取同步状态的线程 |
Node nextWaiter | 条件队列中的后续节点,如果当前节点是共享的,那么这个字段将是一个SHARED变量,也就是说节点类型(独占和共享)和条件队列中的后续节点共用同一个字段 |
LockSupport void park() | 阻塞当前线程,如果调用unpark方法或者当前线程被中断,才能从park方法返回 |
LockSupport void unpark(Thread thread) | 唤醒处于阻塞状态的线程 |
ConditionObject Node firstWaiter | 条件队列首节点 |
ConditionObject Node lastWaiter | 条件队列尾节点 |
void await() | 当前线程进入等待状态直到signal或中断,当前线程将进入运行状态且从await方法返回的情况,包括: 其他线程调用该Condition的signal或者signalAll方法,且当前线程被选中唤醒; 其他线程调用interrupt方法中断当前线程; 如果当前线程从await方法返回表明该线程已经获取了Condition对象对应的锁 |
void awaitUninterruptibly() | 和await方法类似,但是对中断不敏感 |
long awaitNanos(long nanosTimeout) | 当前线程进入等待状态直到被signal、中断或者超时。返回值表示剩余的时间。 |
boolean awaitUntil(Date deadline) | 当前线程进入等待状态直到被signal、中断或者某个时间。如果没有到指定时间就被通知,方法返回true,否则表示到了指定时间,返回false |
void signal() | 唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁 |
void signalAll() | 唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition相关联的锁 |
看到这,我们对AQS的数据结构应该基本上有一个大致的认识,有了这个基本面的认识,我们就可以来看下AQS的源代码。
3**、AQS的源代码实现**
主要通过独占式同步状态的获取和释放、共享式同步状态的获取和释放来看下AQS是如何实现的。
3.1 独占式同步状态的获取和释放
独占式同步状态调用的方法是acquire,代码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上述代码主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作,其主要逻辑是:首先调用子类实现的tryAcquire方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造独占式同步节点(同一时刻只能有一个线程成功获取同步状态)并通过addWaiter方法将该节点加入到同步队列的尾部,最后调用acquireQueued方法,使得该节点以自旋的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
下面来首先来看下节点构造和加入同步队列是如何实现的。代码如下:
private Node addWaiter(Node mode) {
// 当前线程构造成Node节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 尝试快速在尾节点后新增节点 提升算法效率 先将尾节点指向pred
Node pred = tail;
if (pred != null) {
//尾节点不为空 当前线程节点的前驱节点指向尾节点
node.prev = pred;
//并发处理 尾节点有可能已经不是之前的节点 所以需要CAS更新
if (compareAndSetTail(pred, node)) {
//CAS更新成功 当前线程为尾节点 原先尾节点的后续节点就是当前节点
pred.next = node;
return node;
}
}
//第一个入队的节点或者是尾节点后续节点新增失败时进入enq
enq(node);
return 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;
}
}
}
}
节点进入同步队列之后,就进入了一个自旋的过程,每个线程节点都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中并会阻塞节点的线程,代码如下:
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);
}
}
再来看看shouldParkAfterFailedAcquire和parkAndCheckInterrupt是怎么来阻塞当前线程的,代码如下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//前驱节点的状态决定后续节点的行为
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/\*前驱节点为-1 后续节点可以被阻塞
\* 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 {
/\*前驱节点是初始或者共享状态就设置为-1 使后续节点阻塞
\* 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;
}
private final boolean parkAndCheckInterrupt() {
//阻塞线程
LockSupport.park(this);
return Thread.interrupted();
}
节点自旋的过程大致示意图如下,其实就是对图二、图三的补充。
图六 节点自旋获取队列同步状态
整个独占式获取同步状态的流程图大致如下:
图七 独占式获取同步状态
当同步状态获取成功之后,当前线程从acquire方法返回,对于锁这种并发组件而言,就意味着当前线程获取了锁。有获取同步状态的方法,就存在其对应的释放方法,该方法为release,现在来看下这个方法的实现,代码如下:
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) {
/\*
\* 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);
}
独占式释放是非常简单而且明确的。
总结下独占式同步状态的获取和释放:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease方法释放同步状态,然后唤醒头节点的后继节点。
3.2 共享式同步状态的获取和释放
共享式同步状态调用的方法是acquireShared,代码如下:
public final void acquireShared(int arg) {
//获取同步状态的返回值大于等于0时表示可以获取同步状态
//小于0时表示可以获取不到同步状态 需要进入队列等待
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
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);
最后
我还为大家准备了一套体系化的架构师学习资料包以及BAT面试资料,供大家参考及学习
已经将知识体系整理好(源码,笔记,PPT,学习视频)
} finally {
if (failed)
cancelAcquire(node);
最后
我还为大家准备了一套体系化的架构师学习资料包以及BAT面试资料,供大家参考及学习
已经将知识体系整理好(源码,笔记,PPT,学习视频)
[外链图片转存中…(img-EuPLfDRr-1715636643460)]
[外链图片转存中…(img-vH4e3typ-1715636643460)]
[外链图片转存中…(img-sxkwaj7P-1715636643460)]