failed = false;
return interrupted;
}
// shouldParkAfterFailedAcquire 把 node 的前一个节点设置为signal
// 只要前一个节点的状态是 signal 了,那么自己就可以阻塞了
if (shouldParkAfterFailedAcquire(p, node) &&
// 线程是在这个方法中阻塞的,醒来的时候仍在无限 for 循环里面,就能再次自旋尝试获得锁
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果获得 node 的锁失败,将 node 从队列中移除
if (failed)
cancelAcquire(node);
}
}
1.1.4 shouldParkAfterFailedAcquire
shouldParkAfterFailedAcquire,这个方法的主要目的就是把前一个节点的状态置为 SIGNAL,只要前一个节点的状态是 SIGNAL,当前节点就可以阻塞了。
// 当前线程可以安心阻塞的标准,就是前一个节点的线程状态是 signal 了
// 传入的参数 pred 表示前一个节点, node 表示当前节点
/*
-
关键操作:
-
- 确认前一个节点是否有效。无效的话,一直往前寻找状态不是取消的节点
-
- 把前一个节点的状态设置为 signal
-
1、2这两步的操作,有可能一次就成功,也有可能需要外部循环多次才能成功(外面是多个for循环)
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 如果前一个节点 waitStatus 状态已经是 signal 了,直接返回,不需要再自旋了
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
// 找到前一个状态不是取消的节点,因为把当前 node 挂在有效节点的身上
// 因为节点的状态是取消的话,是无效的,是不能作为 node 的前置节点的,所以必须找到 node 的有效节点才行
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
// 否则直接把节点状态设置为 signal
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
acquire 整个过程非常长,代码也非常多,但注释很清楚,可以一行一行仔细看看代码。
1.1.5 总结
acquire 方法大致分为三步:
-
使用 tryAcquire 方法尝试获得锁,获得锁直接返回,获取不到锁的走 2;
-
把当前线程组装成节点(Node),追加到同步队列的尾部(addWaiter);
-
自旋,使同步队列中当前节点的前置节点状态为 signal 后,然后阻塞自己。
acquireShared 整体流程和 acquire 相同,代码也很相似,重复的源码就不贴了,我们就贴出来不一样的代码来,也方便大家进行比较:
- 第一处尝试获得锁的地方,有所不同,排它锁使用的是 tryAcquire 方法,共享锁使用的是 tryAcquireShared 方法,如下图:
- 第二处不同,在于节点获得排它锁时,仅仅把自己设置为同步队列的头节点即可(setHead 方法),但如果是共享锁的话,还会去唤醒自己的后续节点,一起来获得该锁(setHeadAndPropagate 方法),不同之处如下(左边排它锁,右边共享锁):
1.2.1 setHeadAndPropagate
这个方法主要做了两件事:
-
把当前节点设置成头节点
-
看看后续节点有无正在等待,并且也是共享模式的,有的话唤醒这些节点
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
// 当前节点设置成头节点
setHead(node);
// propagate > 0 表示已经有节点获得共享锁了
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 共享模式,还唤醒头节点的后置节点
if (s == null || s.isShared())
doReleaseShared();
}
}
// 释放后置共享节点
private void doReleaseShared() {
for (;😉 {
Node h = head;
// 还没有到队尾,此时队列中至少有两个节点
if (h != null && h != tail) {
int ws = h.waitStatus;
// 如果队列状态是 SIGNAL ,说明后续节点都需要唤醒
if (ws == Node.SIGNAL) {
// CAS 保证只有一个节点可以运行唤醒的操作
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 进行唤醒操作
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
这个就是共享锁独特的地方,当一个线程获得锁后,它就会去唤醒排在它后面的其它节点,让其它节点也能够获得锁。
========================================================================
释放锁的触发时机就是我们常用的 Lock.unLock () 方法,目的就是让线程释放对资源的访问权(流程见整体架构图蓝色路线)。
释放锁也是分为两类,一类是排它锁的释放,一类是共享锁的释放,我们分别来看下。
排它锁的释放就比较简单了,从队头开始,找它的下一个节点,如果下一个节点是空的,就会从尾开始,一直找到状态不是取消的节点,然后释放该节点,源码如下:
2.1.1 release
public final boolean release(int arg) {
// tryRelease 交给实现类去实现,如果返回true则说明成功释放锁
if (tryRelease(arg)) {
Node h = head;
// 头节点不为空 && 非初始化状态
if (h != null && h.waitStatus != 0)
// 从头开始唤醒等待锁的节点
unparkSuccessor(h);
return true;
}
return false;
}
2.1.2 unparkSuccessor
// 当线程释放锁成功后,从 node 开始唤醒同步队列中的节点
// 通过唤醒机制,保证线程不会一直在同步队列中阻塞等待
private void unparkSuccessor(Node node) {
// node节点为当前释放锁的节点,也是同步队列的头结点
int ws = node.waitStatus;
// 如果节点已经被取消了,把节点的状态设置为初始化
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 拿出 node 节点的下一个节点
Node s = node.next;
// s 为空,表示 node 的后一个节点为空
// s.waitStatus > 0 表示s节点已经被取消了
// 遇到上面两种情况,就从队尾开始,向前遍历,找到第一个 waitStatus 字段不是被取消的节点
if (s == null || s.waitStatus > 0) {
s = null;
// 这个for循环,从尾部开始迭代
// 主要是因为节点被阻塞的时候,是在acquiredQueued方法里面被阻塞的(上面已经介绍了这个方法)
// 所以唤醒的时候也一定是在acquiredQueued方法里面被唤醒
// 唤醒的条件是,判断判断当前节点的前置节点是否为头结点,这里是判断当前节点的前置节点
// 所以这里必须从尾部开始迭代,目的就是过滤无效的前置节点,不然节点被唤醒时,发现前置节点是无效节点的话,就又会陷入阻塞
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 唤醒以上代码找到的线程
if (s != null)
LockSupport.unpark(s.thread);
}
2.2.1 releaseShared
释放共享锁的方法是 releaseShared,主要分成两步:
-
tryReleaseShared 尝试释放当前共享锁,失败返回 false,成功走 2;
-
唤醒当前节点的后续阻塞节点,这个方法我们之前看过了,线程在获得共享锁的时候,就会去唤醒其后面的节点,方法名称为:doReleaseShared。
我们一起来看下 releaseShared 的源码:
// 共享模式下,释放当前线程的共享锁
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
// 线程在获得共享锁的时候,就会去唤醒其后面的节点
doReleaseShared();
return true;
}
return false;
}
=================================================================================
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对
象关联的锁。Condition对象是由Lock对象创建出来的,换句话说,Condition是依赖Lock对象的。
当调用Condition的await()方法后,当前线程会释放锁并在此等待。而其他线程调用Condition对象的
signal()方法通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。
因为并不是所有场景一个同步队列就可以搞定的。
-
在遇到锁 + 队列结合的场景时,就需要 Lock + Condition 配合才行,先使用 Lock 来决定哪些线程可以获得锁,哪些线程需要到同步队列里面排队阻塞;
-
获得锁的多个线程在碰到队列满或者空的时候,可以使用 Condition 来管理这些线程,让这些线程阻塞等待,然后在合适的时机后,被正常唤醒。
同步队列 + 条件队列联手使用的场景,最多被使用到锁 + 队列的场景中。所以说条件队列也是不可或缺的一环。
接下来我们来看一下条件队列一些比较重要的方法,以下方法都在 ConditionObject 内部类中。
获得锁的线程,如果在碰到队列满或空的时候,就会阻塞住,这个阻塞就是用条件队列实现的,这个动作我们叫做入条件队列,方法名称为 await,流程见整体架构图中绿色箭头流向,我们一起来看下 await 的源码:
// 线程进入条件队列
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 加入条件队列的队尾
Node node = addConditionWaiter();
// 加入队列后,会释放 lock 时所申请的资源,唤醒同步队列的头结点
// fullyRelease 释放资源,自己马上就要阻塞了,所以要马上释放之前 lock 的资源
// 不然自己不被唤醒的话,别的线程永远得不到该共享的资源
long savedState = fullyRelease(node);
int interruptMode = 0;
// 确认 node 不在同步队列上,在阻塞,如果node在同步队列上的话,是不能够上锁的
// 为什么这么做?
// node刚被加入到条件队列,立马就被其他线程唤醒转移到同步队列当中了
// 线程之前在条件队列中沉睡,被唤醒后加入到同步队列当中
while (!isOnSyncQueue(node)) {
// 阻塞在条件队列上
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 其他线程通过 signal 已经把 node 从条件队列中转移到同步队列中
// 所以这里尝试acquireQueued
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
// 如果状态不是CONDITION,就会自动删除
// CONDITION表示线程正在等待条件
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
await 方法有几点需要特别注意:
-
fullyRelease(node);,节点在准备进入条件队列之前,一定会先释放当前持有的锁,不然自己进去条件队列了,其余的线程都无法获得锁了;
-
(acquireQueued(node, savedState) && interruptMode != THROW_IE),此时节点是被 Condition.signal 或者 signalAll 方法唤醒的,此时节点已经成功的被转移到同步队列中去了(整体架构图中蓝色流程),所以可以直接执行 acquireQueued 方法;
-
Node 在条件队列中的命名,源码喜欢用 Waiter 来命名,所以我们在条件队列中看到 Waiter,其实就是 Node。
await 方法中有两个重要方法:addConditionWaiter 和 unlinkCancelledWaiters,我们一一看下。
addConditionWaiter 方法主要是把节点放到条件队列中,方法源码如下:
// 增加新的 waiter 到队列中,返回新添加的 waiter
// 如果尾节点的状态不是CONDITION状态,删除条件队列中所有状态不是CONDITION的节点
// 如果队列为空,新增的节点作为队列头节点,否则追加到尾节点上
private Node addConditionWaiter() {
Node t = lastWaiter;
// 如果尾节点的状态不是CONDITION状态,删除
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 新建条件队列node
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// 如果是空的,直接放在队列头
if (t == null)
firstWaiter = node;
// 队列不为空,放在队列尾
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
整体过程比较简单,就是追加到队列的尾部
其中还有个重要方法叫做 unlinkCancelledWaiters,这个方法会删除掉条件队列中状态不是 CONDITION 的所有节点,我们来看下 unlinkCancelledWaiters 方法的源码,如下:
// 检查尾部的 waiter 是不是已经不是CONDITION状态
// 如果不是,则会删除
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
// trail 表示上一个状态,可以将状态为 CONDITION 的node串联起来,即使node之间有其他的节点也可以访问
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
// 当前 node 的状态不是CONDITION,则会删除自己
if (t.waitStatus != Node.CONDITION) {
// 删除当前 node
t.nextWaiter = null;
// 如果 trail 是空的,则循环从头开始,说明当前节点的状态都不是 CONDITION
// 都已经被删除了。所以移动队列的头结点到当前节点的下一个节点
if (trail == null)
firstWaiter = next;
// 如果找到上次状态是CONDITION的节点的话,先把当前节点删掉
// 然后把自己挂到上一个状态是CONTION的节点上
else
trail.nextWaiter = next;
// 遍历结束,最后一次找到的CONDITION节点就是尾节点
if (next == null)
lastWaiter = trail;
}
// 状态是CONDITION的node
else
trail = t;
// 继续循环,循环顺序从头到尾
t = next;
}
}
signal 方法是唤醒的意思,比如之前队列满了,有了一些线程因为 take 操作而被阻塞进条件队列中,突然队列中的元素被线程 A 消费了,线程 A 就会调用 signal 方法,唤醒之前阻塞的线程,会从条件队列的头节点开始唤醒(流程见整体架构图中黄色部分),源码如下:
// 唤醒阻塞在条件队列中的节点
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 从头节点开始唤醒
Node first = firstWaiter;
if (first != null)
// doSignal 方法会把条件队列中的节点转移到同步队列中去
doSignal(first);
}
// 把条件队列头节点转移到同步队列去
private void doSignal(Node first) {
感受:
其实我投简历的时候,都不太敢投递阿里。因为在阿里一面前已经过了字节的三次面试,投阿里的简历一直没被捞,所以以为简历就挂了。
特别感谢一面的面试官捞了我,给了我机会,同时也认可我的努力和态度。对比我的面经和其他大佬的面经,自己真的是运气好。别人8成实力,我可能8成运气。所以对我而言,我要继续加倍努力,弥补自己技术上的不足,以及与科班大佬们基础上的差距。希望自己能继续保持学习的热情,继续努力走下去。
也祝愿各位同学,都能找到自己心动的offer。
分享我在这次面试前所做的准备(刷题复习资料以及一些大佬们的学习笔记和学习路线),都已经整理成了电子文档
程 A 消费了,线程 A 就会调用 signal 方法,唤醒之前阻塞的线程,会从条件队列的头节点开始唤醒**(流程见整体架构图中黄色部分),源码如下:
// 唤醒阻塞在条件队列中的节点
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 从头节点开始唤醒
Node first = firstWaiter;
if (first != null)
// doSignal 方法会把条件队列中的节点转移到同步队列中去
doSignal(first);
}
// 把条件队列头节点转移到同步队列去
private void doSignal(Node first) {
感受:
其实我投简历的时候,都不太敢投递阿里。因为在阿里一面前已经过了字节的三次面试,投阿里的简历一直没被捞,所以以为简历就挂了。
特别感谢一面的面试官捞了我,给了我机会,同时也认可我的努力和态度。对比我的面经和其他大佬的面经,自己真的是运气好。别人8成实力,我可能8成运气。所以对我而言,我要继续加倍努力,弥补自己技术上的不足,以及与科班大佬们基础上的差距。希望自己能继续保持学习的热情,继续努力走下去。
也祝愿各位同学,都能找到自己心动的offer。
分享我在这次面试前所做的准备(刷题复习资料以及一些大佬们的学习笔记和学习路线),都已经整理成了电子文档
[外链图片转存中…(img-ytFRF8L5-1714273541080)]