小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
条件锁,是指在获取锁之后发现当前业务场景自己无法处理,而需要等待某个条件的出现才可以继续处理时使用的一种锁。
Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。不同的是,Object中的这些方法是和同步锁捆绑使用的;而Condition是需要与互斥锁/共享锁捆绑使用的。
先来看下示例,看看条件锁的基本使用
public class ConditionDemo {
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
Condition condition1 = reentrantLock.newCondition();
new Thread(new Runnable() {
@Override
public void run() {
reentrantLock.lock(); // 1
try {
condition1.await(); // 2
} catch (Exception e) {
} finally {
System.out.println("释放咯"); // 6
reentrantLock.unlock();
}
}
}).start();
Thread.sleep(1000);
reentrantLock.lock(); // 3
try {
System.out.println("我来释放你"); // 4
condition1.signal(); // 5
} catch (Exception e) {
} finally {
reentrantLock.unlock(); // 7
}
}
}
输出结果:
我来释放你
释放咯
复制代码
上面的代码很简单,一个线程等待条件,另一个线程通知条件已成立,后面的数字代表代码实际运行的顺序,如果你能把这个顺序看懂基本条件锁的用法就掌握得差不多了。
主要属性
条件锁维护了一个条件队列,firstWaiter指向条件队列头节点,
lastWaiter指向条件队列尾节点。
新建条件锁
// ReentrantLock.newCondition()
public Condition newCondition() {
return sync.newCondition();
}
// ReentrantLock.Sync.newCondition()
final ConditionObject newCondition() {
return new ConditionObject();
}
// AQS.ConditionObject
public class ConditionObject implements Condition, java.io.Serializable {}
复制代码
新建一个条件锁最后就是调用的AQS的ConditionObject类完成实例化,从这也可以看到条件锁最终的实现都是由ConditionObject来完成的。
await方法
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 将当前线程加入到条件队列中
Node node = addConditionWaiter();
// 完全释放当前线程对应的锁,并唤醒下一个节点,将state置为0
int savedState = fullyRelease(node);
//0 默认值,condition队列挂起期间未接收过中断信号
// -1 在condition队里挂起期间接收到中断信号
// 1 在condition 队列挂起期间未接收到中断信号,但在迁移到阻塞队列之后接收过中断信号
int interruptMode = 0;
// TRUE:isOnSyncQueue:当前Node还在阻塞队列
// FALSE:isOnSyncQueue: 当前Node还在条件队列中
while (!isOnSyncQueue(node)) {
// 挂起当前Node对应的线程
LockSupport.park(this);
//什么时候会被唤醒?都有几种情况
// 1.外部线程获取到lock之后,调用signal(),会将条件队列的头节点转移到阻塞队列
// 2. 转移至阻塞队列后,发现阻塞队列的前驱节点状态是取消状态,此时会唤醒当前节点
// 就算在condition队列挂起期间 线程发生了中断,对应的node也会被迁移到“阻塞队列”。
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 执行到这里,说明当前Node已经迁移到阻塞队列
// 自旋直到获取到锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 清除取消的节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 挂起期间 发生过中断(1.条件队列内的挂起,2.条件队列之外的挂起)
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
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);
// 如果尾节点为空,则把新节点赋值给头节点(相当于初始化队列)
// 否则把新节点赋值给尾节点的nextWaiter指针
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
// 尾节点指向新节点
lastWaiter = node;
// 返回新节点
return node;
}
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
// 释放获得的锁,唤醒下一个节点
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
final boolean isOnSyncQueue(Node node) {
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
if (node.next != null) // If has successor, it must be on queue
return true;
/**
* 执行到这里 waitStatus==0
*
*findNodeFromTail:从阻塞队列的末尾往前查找Node,查找到返回true
* 查找不到返回false
*/
return findNodeFromTail(node);
}
复制代码
-
首先加入条件队列
加入条件队列会加入队列尾节点,并将节点的waitstatus状态设置condition状态
-
完全释放锁
可以看到这里是调用的fullyRelease方法,而不是release方法,为什么呢,你想一下,如果出现可重入的情况,那么当前的state值就不是1了,所以fullyRelease方法就是为了完全的释放,保证彻底释放该线程持有的锁,并唤醒下一个线程。
-
判断当前线程在条件队列还是阻塞队列
-
如果在条件队列,就要将其park(),等待唤醒。
-
如果现在在阻塞队列,说明当前节点已经被迁移到阻塞队列,就调用上一篇文章说的acquireQueued方法自旋获取锁。
signal方法
// AQS.signal()
public final void signal() {
// 如果不是当前线程占有着锁,调用这个方法会出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 条件队列的头节点
Node first = firstWaiter;
// 如果有等待条件的节点,则通知它条件已成立
if (first != null)
doSignal(first);
}
// ReentrantLock.isHeldExclusively()
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
// AQS. doSignal()
private void doSignal(Node first) {
do {
if ((firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
//当前first节点处条件队列,断开和下一个节点的关系
first.nextWaiter = null;
// transferForSignal true:表示当前first节点迁移到阻塞队列成功
// first = firstWaiter) != null:当前first迁移失败,即将first更新为first.next 继续尝试迁移
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
// AQS.transferForSignal()
final boolean transferForSignal(Node node) {
/**
* 成功:当前节点在条件队列中状态正常
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 将node放入到阻塞队列当中
// 返回值是当前节点的前置节点
Node p = enq(node);
// 获取前驱结点状态
int ws = p.waitStatus;
// 前驱节点只要不是0或者-1 那么,就唤醒当前线程
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
复制代码
-
从条件队列的头节点开始寻找一个非取消状态的节点;
-
把它从条件队列移到AQS队列;
-
且只移动一个节点;
signalAll方法
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
复制代码
signalAll方法是唤醒所有在条件队列中的结点,将他们一个一个加入阻塞队列。
总结
-
条件锁是指为了等待某个条件出现而使用的一种锁;
-
条件锁比较经典的使用场景就是队列为空时阻塞在条件notEmpty上;
-
ReentrantLock中的条件锁是通过AQS的ConditionObject内部类实现的;
-
await()和signal()方法都必须在获取锁之后释放锁之前使用;
-
await()方法会新建一个节点放到条件队列中,接着完全释放锁,然后阻塞当前线程并等待条件的出现;
-
signal()方法会寻找条件队列中第一个可用节点移到AQS队列中;
-
在调用signal()方法的线程调用unlock()方法才真正唤醒阻塞在条件上的节点(此时节点已经在AQS队列中);
-
之后该节点会再次尝试获取锁,后面的逻辑与lock()的逻辑基本一致了。