Condition
Condition接口
在并发编程中,每个Java对象都存在一组监视器方法,如wait()
、notify()
以及notifyAll()
方法,通过这些方法,我们可以实现线程间通信与协作(也称为等待唤醒机制),如生产者-消费者模式,而且这些方法必须配合着synchronized
关键字使用。
与synchronized的等待唤醒机制
相比,Condition具有更多的灵活性以及精确性,这是因为notify()
在唤醒线程时是随机(同一个锁),而Condition则可通过多个Condition实例对象建立更加精细的线程控制,也就带来了更多灵活性:
-
通过Condition能够精细的控制多线程的休眠与唤醒。
-
对于一个锁,可以为多个线程间建立不同的Condition。
Condition是一个接口:
public interface Condition {
/**
* 使当前线程进入等待状态,直到【被通知(signal)】或【中断】
* 当其他线程调用singal()或singalAll()方法时,该线程将被唤醒
* 当其他线程调用interrupt()方法中断当前线程
* await()相当于synchronized等待唤醒机制中的wait()方法
*/
void await() throws InterruptedException;
// 当前线程进入等待状态,直到被唤醒,该方法【不响应中断要求】
void awaitUninterruptibly();
// 调用该方法,当前线程进入等待状态,直到【被唤醒】或【被中断】或【超时】
// 其中nanosTimeout指的等待超时时间,单位纳秒
long awaitNanos(long nanosTimeout) throws InterruptedException;
// 同awaitNanos,但可以指明时间单位
boolean await(long time, TimeUnit unit) throws InterruptedException;
// 调用该方法当前线程进入等待状态,直到被唤醒、中断或到达某个时间期限(deadline)
// 如果没到指定时间就被唤醒,返回true,其他情况返回false
boolean awaitUntil(Date deadline) throws InterruptedException;
// 唤醒一个等待在Condition上的线程,
// 该线程从等待方法返回前,必须获取与Condition相关联的锁,功能与notify()相同
void signal();
// 唤醒所有等待在Condition上的线程,
// 该线程从等待方法返回前,必须获取与Condition相关联的锁,功能与notifyAll()相同
void signalAll();
}
同步队列与等待队列
AQS中存在两种队列,一种是同步队列
,一种是等待队列
,而等待队列就相对于Condition而言的。在使用Condition前必须获得锁,同时在Condition的等待队列
上的结点与前面同步队列
的结点是同一个类即Node,其结点的waitStatus的值为CONDITION=1
。
同步队列与等待队列的关系:
每个Condition都对应着一个等待队列,也就是说如果一个锁上创建了多个Condition对象,那么也就存在多个等待队列。等待队列是一个FIFO的队列,在队列中每一个节点都包含了一个线程的引用,而该线程就是Condition对象上等待的线程。
当一个线程调用了await()
相关的方法,那么该线程将会释放锁
,并构建一个Node节点
封装当前线程的相关信息,加入到等待队列
中进行等待,直到被唤醒
、中断
、超时
才从队列中移出。
Condition中的等待队列模型如下:
Node节点的数据结构,在等待队列中使用的变量与同步队列是不同的,Condtion中等待队列的结点,只有直接指向的后继结点,并没有指明前驱结点,而且使用的变量是nextWaiter
而不是next。
等待队列
中结点的状态只有两种即CANCELLED
和CONDITION
,前者表示线程已结束,需要从等待队列中移除
,后者表示条件结点等待被唤醒
。
每个Codition对象对应于一个等待队列,也就是说AQS中只能存在一个同步队列,但可拥有多个等待队列。
newCondition
public class ReentrantLock implements Lock, java.io.Serializable {
public Condition newCondition() {
// 使用自定义的条件
return sync.newCondition();
}
}
public class MyMutex implements Lock {
private static class MySync extends AbstractQueuedSynchronizer {
/**
* 主要用于等待/通知机制,每个condition都有一个与之对应的条件等待队列
* @return condition
*/
Condition newCondition() {
return new ConditionObject();
}
}
}
public class ReentrantLock implements Lock, java.io.Serializable {
final ConditionObject newCondition() {
return new ConditionObject();
}
}
AQS#ConditionObject
ConditionObject
是Condition的实现类,该类就定义在了AQS中。在实现类ConditionObject中有两个结点,分别是firstWaiter
和lastWaiter
,firstWaiter代表等待队列第一个等待结点
,lastWaiter代表等待队列最后一个等待结点
:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer implements java.io.Serializable {
public class ConditionObject implements Condition, java.io.Serializable {
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
public ConditionObject() { }
// ...
}
}
所以,只需要来看一下ConditionObject实现的await/signal
方法来使用这两个成员变量就可以了。
await()
await()
方法主要做了3件事:
-
一是调用
addConditionWaiter()
方法将当前线程封装成node结点加入等待队列; -
二是调用
fullyRelease(node)
方法释放同步状态,并唤醒后继结点的线程; -
三是调用
isOnSyncQueue(node)
方法判断结点是否在同步队列
中。注意是个while循环,如果同步队列中没有该结点就直接挂起该线程,需要明白的是如果线程被唤醒后就调用acquireQueued(node, savedState)
执行自旋操作争取锁,即当前线程结点从等待队列转移到同步队列并开始努力获取锁。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer implements java.io.Serializable {
public class ConditionObject implements Condition, java.io.Serializable {
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
public ConditionObject() { }
// ...
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter(); // 1.构建Node节点,并加入到等待队列中
int savedState = fullyRelease(node); // 2.释放当前线程锁,即释放同步状态
int interruptMode = 0;
while (!isOnSyncQueue(node)) { // 3.判断结点是否在同步队列(SyncQueue)中,即是否被唤醒
LockSupport.park(this); // 挂起当前线程
// 判断是否被中断唤醒,如果是退出循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 被唤醒后执行自旋操作争取获得锁,同时判断线程是否被中断
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters(); // 清理等待队列中不为CONDITION状态的结点
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
}
}
执行addConditionWaiter()
添加到等待队列:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer implements java.io.Serializable {
public class ConditionObject implements Condition, java.io.Serializable {
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
/**
* Adds a new waiter to wait queue.
* @return its new wait node
*/
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) { // 判断是否为结束状态的结点并移除
unlinkCancelledWaiters();
t = lastWaiter;
}
// 新构建的节点的waitStatus是CONDITION,注意不是0或SIGNAL了
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// 构建单向同步队列
if (t == null)
firstWaiter = node; // 加入等待队列
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
}
}
为什么这里是单向队列,也没有使用CAS来保证加入队列的安全性呢?
因为await
是Lock范式try
中使用的,说明已经获取到锁了,所以就没必要使用CAS了。至于是单向,因为这里还不涉及到竞争锁,只是做一个条件等待队列。
线程已经按相应的条件加入到了条件等待队列中,那如何再尝试获取锁呢?
signal/signalAll
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer implements java.io.Serializable {
public class ConditionObject implements Condition, java.io.Serializable {
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
public final void signal() {
if (!isHeldExclusively()) // 判断是否持有独占锁,如果不是抛出异常
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null) // 唤醒等待队列第一个结点的线程
doSignal(first);
}
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
}
}
这里signal()
方法做了两件事:
-
一是判断当前线程是否持有独占锁,没有就抛出异常,从这点也可以看出只有独占模式先采用等待队列,而共享模式下是没有等待队列的,也就没法使用Condition。
-
二是唤醒等待队列的第一个结点,即执行
doSignal(first)
。
doSignal/doSignalAll
doSignal(first)
方法中做了两件事:
-
一是从条件等待队列移除被唤醒的节点,然后
重新维护
条件等待队列的firstWaiter和lastWaiter的指向。 -
二是将从等待队列移除的结点加入同步队列(在
transferForSignal()
方法中完成的),如果进入到同步队列失败
,并且条件等待队列还有不为空的节点
,则继续循环唤醒后续
其他结点的线程。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer implements java.io.Serializable {
public class ConditionObject implements Condition, java.io.Serializable {
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
private void doSignal(Node first) {
do {
// 移除条件等待队列中的第一个结点,
// 如果后继结点为null,那么说没有其他结点,将尾结点也设置为null
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
// 如果被通知节点没有进入到同步队列,并且条件等待队列还有不为空的节点,则继续循环通知后续结点
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
/*
* 循环判断是否还有nextWaiter,
* 如果有就像signal操作一样,将其从【条件等待队列】中移到【同步队列】中
*/
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
}
final boolean transferForSignal(Node node) {
// 尝试设置唤醒结点的waitStatus为0,即初始化状态
// 如果设置失败,说明当前结点node的waitStatus已不为CONDITION状态,那么只能是结束状态了,因此返回false
// 返回doSignal()方法中继续唤醒其他结点的线程,注意这里并不涉及并发问题,
// 所以CAS操作失败只可能是预期值不为CONDITION,
// 而不是多线程设置导致预期值变化,毕竟操作该方法的线程是持有锁的。
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 加入同步队列并返回前驱结点p
Node p = enq(node);
int ws = p.waitStatus;
// 判断前驱结点是否为结束结点(CANCELLED=1)或者
// 在设置前驱节点状态为Node.SIGNAL状态失败时,唤醒被通知节点代表的线程
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 唤醒node结点的线程
LockSupport.unpark(node.thread);
return true;
}
}
总结:signal的唤醒过程
signal()
被调用后,先判断当前线程是否持有独占锁,如果有,那么唤醒当前Condition对象中等待队列的第一个结点的线程
,并从等待队列中移除
该结点,移动到同步队列
中:
-
如果加入同步队列失败,那么继续
循环唤醒
等待队列中的其他结点的线程; -
如果成功加入同步队列,那么如果其前驱节点是已结束的节点或者设置前驱节点状态为Node.SIGNAL状态失败,则通过
LockSupport.unpark()
唤醒被通知节点代表的线程,到此signal()
任务完成。
被唤醒后的线程,将从前面的await()
方法中的while循环中退出,因为此时该线程的结点已在同步队列中,那么while (!isOnSyncQueue(node))
将不在符合循环条件,进而调用AQS的acquireQueued()
方法加入获取同步状态的竞争中,这就是等待唤醒机制的整个流程实现原理,流程如下图所示(注意无论是同步队列还是等待队列使用的Node数据结构都是同一个,不过是使用的内部变量不同罢了):
生产者-消费者Condition
package condition;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;
import static java.lang.Thread.currentThread;
import static java.util.concurrent.ThreadLocalRandom.current;
public class TestCondition {
// 定义显示锁
private static final ReentrantLock lock = new ReentrantLock();
// 创建与显式锁关联的Condition对象
private static final Condition condition = lock.newCondition();
// 链表
private static final LinkedList<Long> list = new LinkedList<>();
// 链表最大容量为100
private static final int CAPACITY = 100;
// 定义数据的初始值
private static long i = 0;
/**
* 生产者方法
*/
private static void produce() {
// 获取锁
lock.lock();
try {
// 当链表中数据量达到100时,生产者线程将被阻塞,加入与Condition关联的wait队列中
while (list.size() >= CAPACITY) {
condition.await();
}
// 当链表中数据量不足100时,生产新的数据
i++;
list.addLast(i);
System.out.println(currentThread().getName() + " 生产了数据 " + i);
// 1. 通知其他线程
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
}
/**
* 消费者方法
*/
private static void consume() {
lock.lock();
try {
// 当list中数据为空时,消费者线程将被阻塞加入与Condition关联的wait队列
while (list.isEmpty()) {
condition.await();
}
// 消费数据
Long value = list.removeFirst();
System.out.println(currentThread().getName() + " 消费了数据 " + value);
// 2.通知其他线程
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private static void sleep() {
try {
TimeUnit.SECONDS.sleep(current().nextInt(5));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 启动10个生产者线程
IntStream.range(0, 10).forEach(i ->
new Thread(
() -> {
for (; ; ) {
produce();
sleep();
}
}, "Producer-" + i
).start()
);
// 启动5个消费者线程
IntStream.range(0, 5).forEach(i ->
new Thread(
() -> {
for (; ; ) {
consume();
sleep();
}
}, "Consumer-" + i
).start()
);
}
}
输出结果:
...
Producer-6 生产了数据 8
Producer-4 生产了数据 9
Producer-8 生产了数据 10
Consumer-0 消费了数据 1
Consumer-1 消费了数据 2
Consumer-2 消费了数据 3
Consumer-4 消费了数据 4
Consumer-3 消费了数据 5
Producer-2 生产了数据 11
Producer-6 生产了数据 12
...
注释1和2处condition.signalAll()
唤醒的是与Condition关联的阻塞队列中的所有阻塞线程。由于使用的是唯一的一个Condition实例,因此,生产者唤醒的有可能是与Condition关联的wait队列中的生产者线程。假设此时生产者线程被唤醒并抢到了CPU的调度获得了执行权,但又发现队列已满再次进入阻塞。这样的线程上下文开销实际上是没有意义的,甚至会影响性能(多线程下的线程上下文切换开销是非常大的性能损耗)。
因此,需要使用两个Condition对象,一个用于队列已满临界值条件的处理,另外一个用于队列为空的临界值条件的处理。此时,在生产者中唤醒的阻塞线程只能是消费者线程;在消费者中唤醒的也只能是生产者线程:
package condition;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;
import static java.lang.Thread.currentThread;
import static java.util.concurrent.ThreadLocalRandom.current;
public class TestCondition {
// 定义显示锁
private static final ReentrantLock lock = new ReentrantLock();
// 创建与显式锁关联的Condition对象
private static final Condition Full_Condition = lock.newCondition();
private static final Condition EMPTY_Condition = lock.newCondition();
// 链表
private static final LinkedList<Long> list = new LinkedList<>();
// 链表最大容量为100
private static final int CAPACITY = 100;
// 定义数据的初始值
private static long i = 0;
/**
* 生产者方法
*/
private static void produce() {
// 获取锁
lock.lock();
try {
// 当链表中数据量达到100时,生产者线程将被阻塞,加入Full_Condition wait队列中
while (list.size() >= CAPACITY) {
Full_Condition.await();
}
// 当链表中数据量不足100时,生产新的数据
i++;
list.addLast(i);
System.out.println(currentThread().getName() + " 生产了数据 " + i);
// 1. 生产者线程通知消费者线程
EMPTY_Condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
}
/**
* 消费者方法
*/
private static void consume() {
lock.lock();
try {
// 当list中数据为空时,消费者线程将被阻塞加入EMPTY_Condition wait队列
while (list.isEmpty()) {
EMPTY_Condition.await();
}
// 消费数据
Long value = list.removeFirst();
System.out.println(currentThread().getName() + " 消费了数据 " + value);
// 2.消费者线程通知生产者线程
Full_Condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private static void sleep() {
try {
TimeUnit.SECONDS.sleep(current().nextInt(5));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 启动10个生产者线程
IntStream.range(0, 10).forEach(i ->
new Thread(
() -> {
for (; ; ) {
produce();
sleep();
}
}, "Producer-" + i
).start()
);
// 启动5个消费者线程
IntStream.range(0, 5).forEach(i ->
new Thread(
() -> {
for (; ; ) {
consume();
sleep();
}
}, "Consumer-" + i
).start()
);
}
}
输出结果:
...
Producer-6 生产了数据 7
Producer-5 生产了数据 8
Producer-8 生产了数据 9
Producer-9 生产了数据 10
Consumer-0 消费了数据 1
Consumer-3 消费了数据 2
Consumer-4 消费了数据 3
Consumer-1 消费了数据 4
Consumer-2 消费了数据 5
Consumer-0 消费了数据 6
Producer-8 生产了数据 11
...