概述
任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括 wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与 synchronized 同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。
通过对比Object的监视器方法和Condition接口,可以更详细地了解Condition的特性,对比项与结果如下表。
- Object对象 wait方法、notify方法
- 前置条件:获取对象的锁
- Condition对象 await方法 signal 方法
- 前置条件:调用Lock.lock() 获取锁
- LockSupport对象 park方法和 unpark方法
1、Condition的使用
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。 Condition的使用方式比较简单,需要注意在调用方法前获取锁。
import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * Condition接口与示例 * @param <T> */ public class BoundedQueue<T> { // 线程池 private static ExecutorService THREAD_POOL = new ThreadPoolExecutor(2, 4, 60, TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>(1000), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); // 对象数组 private Object[] items; // 添加的下标,删除的下标和数组当前数量 private int addIndex, removeIndex, count; private Lock lock = new ReentrantLock(); // 定义一个可重入锁 private Condition notEmpty = lock.newCondition(); // 添加一个Condition private Condition notFull = lock.newCondition(); // 添加一个Condition public BoundedQueue(int size) { items = new Object[size]; } /** * 添加一个元素,如果数组满,则添加线程进入等待状态,直到有"空位" * @param t * @throws InterruptedException */ public void add(T t) throws InterruptedException { lock.lock(); // 获取锁 try { // 场景1:如果数组满了,notFull进入等待 while (count == items.length) { System.out.println("items满了,add方法进入等待."); notFull.await(); // 等待remove方法里的notFull.signal() } // 场景2:item添加对象 items[addIndex] = t; if (++addIndex == items.length) // 调整数组索引,避免越界 addIndex = 0; ++count; // count+1,代表添加了一个对象 notEmpty.signal(); // 走到这里,数组里至少有1个对象,必不为空,因此唤醒notEmpty } finally { System.out.println("add: " + t); lock.unlock(); // 释放锁 } } /** * 由头部删除一个元素,如果数组空,则删除线程进入等待状态, * 直到有新添加元素(注意这里并没有真的删除元素,只是把count-1当作是删除) * @return * @throws InterruptedException */ @SuppressWarnings("unchecked") public T remove() throws InterruptedException { lock.lock(); // 获取锁 try { //场景1:如果数组为空,notEmpty进入等待 while (count == 0) { System.out.println("items为空,remove方法进入等待."); notEmpty.await(); // 等待add方法里的notEmpty.signal() } //场景2:如果数组非空,item移除对象 Object x = items[removeIndex]; // item移除对象(假移除) if (++removeIndex == items.length) // 调整数组索引,避免越界 removeIndex = 0; --count; // count-1,代表移除了一个对象 notFull.signal(); // 走到这里,数组里至少有1个空位,必不为满,因此唤醒notFull return (T) x; } finally { System.out.println("remove"); lock.unlock(); // 释放锁 } } public static void main(String args[]) throws InterruptedException { int count = 3; // 可以加大数组的size来看更多的过程 BoundedQueue<Integer> bq = new BoundedQueue<Integer>(count); // 开启一个线程执行添加操作 THREAD_POOL.submit(new Callable<Object>() { public Object call() throws InterruptedException { for (int i = 0; i < count * 2; i++) { bq.add(i); Thread.sleep(200); // 通过睡眠来制造添加和移除的速度差 } return null; } }); // 开启一个线程执行移除操作 THREAD_POOL.submit(new Callable<Object>() { public Object call() throws InterruptedException { Thread.sleep(1000); for (int i = 0; i < count * 2; i++) { bq.remove(); Thread.sleep(50); // 通过睡眠来制造添加和移除的速度差 } return null; } }); } }
输出如下,由于调用remove方法的线程先睡眠了1秒,所以,add方法会先将item数组填满,填满后notFull进入等待。之后,remove方法的线程醒来开始进行移除,当移除之后会唤醒notFull,此时add和remove是并发操作的,但是由于remove的速度更快(通过sleep控制,add每次要睡200毫秒,remove只要50毫秒),所以items必然会被移除到为空,此时notEmpty进入等待,直到add方法往item添加了对象,如此反复。
2、重要入口方法
Condition的实现主要包括:条件队列、等待和通知。其中条件队列放的是AQS里的Node数据结构,使用nextWaiter来维护条件队列。等待和通知共有7个方法。
- signal():唤醒该条件队列的头节点。
- signalAll():唤醒该条件队列的所有节点。
- awaitUninterruptibly():等待,此方法无法被中断,必须通过唤醒才能解除阻塞。
- await():当前线程进入等待。
- awaitNanos(long):当前线程进入等待,有超时时间,入参的单位为纳秒。
- awaitUntil(Date):当先线程进入等待,直到当前时间超过入参的时间。
- await(long, TimeUnit):当前线程进入等待,有超时时间,入参可以自己设置时间单位。
这些方法其实大同小异,因此本文只对常用的signal()、signalAll()和await()方法展开详解。搞懂了这3个方法,搞懂其他几个方法也基本没什么阻碍。
3、基础属性
Condition的实现是ConditionObject,而ConditionObject是同步器AbstractQueuedSynchronizer
的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列(以下称为条件队列),该队列是Condition对象实现等待/通知功能的关键。
private static final long serialVersionUID = 1173984872572414699L; /** First node of condition queue. */ private transient Node firstWaiter; // 条件队列的头节点 /** Last node of condition queue. */ private transient Node lastWaiter; // 条件队列的尾节点 /** * Creates a new {@code ConditionObject} instance. */ public ConditionObject() { }
通过源码可知,条件队列的节点使用的是AQS的Node数据结构。
另外,由于ConditionObject是AQS的内部类,因此必然和AQS是有很多关联的,因此看本文之前必须先了解AQS的实现原理。(如果你对AQS不熟悉,可以参考我的这一篇文章:JUC第十二讲:JUC锁: 锁核心类AQS详解)
条件队列的基本数据结构如下图中的“条件队列”:
4、重点方法
4.1、await方法
// 阻塞当前线程,直接被唤醒或被中断 public final void await() throws InterruptedException { // 如果当前线程被中断过,则抛出中断异常 if (Thread.interrupted()) throw new InterruptedException(); // 添加一个waitStatus为CONDITION的节点到条件队列尾部 Node node = addConditionWaiter(); int savedState = fullyRelease(node); // 释放操作。我们知道只有在拥有锁(acquire成功)的时候才能调用await()方法,因此,调用await()方法的线程的节点必然是同步队列的头节点。所以,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的条件队列中。 // 0为正常,被中断值为THROW_IE或REINTERRUPT int interruptMode = 0; // isOnSyncQueue:判断node是否在同步队列(注意和条件队列区分。调用signal方法会将节点从条件队列移动到同步队列,因此这边就可以跳出while循环) while (!isOnSyncQueue(node)) { // node如果不在同步队列则进行park(阻塞当前线程) LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) // 检查线程被唤醒是否是因为被中断,如果是则跳出循环,否则会进行下一次循环,因为被唤醒前提是进入同步队列,所以下一次循环也必然会跳出循环 break; } if (acquireQueued(node, savedState) && interruptMode != THROW_IE) // acquireQueued返回true代表被中断过,如果中断模式不是THROW_IE,则必然为REINTERRUPT(见上面的checkInterruptWhileWaiting方法) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); // 移除waitStatus为CANCELLED的节点 if (interruptMode != 0) // 如果跳出while循环是因为被中断 reportInterruptAfterWait(interruptMode); // 则根据interruptMode,选择抛出InterruptedException 或 重新中断当前线程 }
- 1、如果当前线程被中断过,则抛出中断异常。
- 2、调用addConditionWaiter方法(详解见下文addConditionWaiter方法)添加一个waitStatus为CONDITION的节点到条件队列尾部。
- 3、调用fullyRelease方法(详解见下文fullyRelease方法)释放锁。
- 4、调用isOnSyncQueue方法(详解见下文isOnSyncQueue方法)来阻塞线程,直到被唤醒或被中断。
- 5、调用acquireQueued方法(详解见acquireQueued方法详解)来尝试获取锁,并判断线程跳出while循环是被唤醒还是被中断。
- 6、如果跳出while循环是因为被中断,则根据interruptMode,选择抛出InterruptedException 或 重新中断当前线程。
4.2、addConditionWaiter方法
// 添加一个waitStatus为CONDITION的节点到条件队列尾部 private Node addConditionWaiter() { Node t = lastWaiter; // If lastWaiter is cancelled, clean out. if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); // 移除waitStatus不为 CONDITION的节点(条件队列里的节点 waitStatus 都为 CONDITION) t = lastWaiter; // 将t赋值为移除了waitStatus不为CONDITION后的尾节点(上面进行了移除操作,因此尾节点可能会发生变化) } // 以当前线程新建一个waitStatus为CONDITION的节点 Node node = new Node(Thread.currentThread(), Node.CONDITION); // t为空,代表条件队列为空 if (t == null) // 将头节点赋值为node firstWaiter = node; else // 否则,队列不为空。将t(原尾节点)的后继节点赋值为node t.nextWaiter = node; lastWaiter = node; // 将node赋值给尾节点,即将node放到条件队列的尾部。这里没有用CAS来保证原子性,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的 return node; }
- 1、如果条件队列的尾节点不为null并且waitStatus不为CONDITION,则调用unlinkCancelledWaiters方法(详解见下文unlinkCancelledWaiters方法)移除waitStatus不为CONDITION的节点(条件队列里的节点waitStatus都为CONDITION),并将t赋值为移除了waitStatus不为CONDITION后的尾节点(上面进行了移除操作,因此尾节点可能会发生变化)
- 2、以当前线程新建一个waitStatus为CONDITION的节点。
- 3、如果t为空,代表条件队列为空,将头节点赋值为node;否则,队列不为空。将t(原尾节点)的后继节点赋值为node。
- 4、最后将node赋值给尾节点,即将node放到条件队列的尾部。这里没有用CAS来保证原子性,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。
4.3、unlinkCancelledWaiters方法
// 从条件队列移除所有waitStatus不为CONDITION的节点 private void unlinkCancelledWaiters() { // t赋值为条件队列的尾节点 Node t = firstWaiter; Node trail = null; while (t != null) { // 向下遍历 Node next = t.nextWaiter; // 如果t的waitStatus不为CONDITION if (t.waitStatus != Node.CONDITION) { // 断开t与t后继节点的关联 t.nextWaiter = null; if (trail == null) // 如果trail为null,则将firstWaiter赋值为next节点,此时还没有遍历到waitStatus为CONDITION的节点,因此直接移动firstWaiter的指针即可移除前面的节点 firstWaiter = next; else trail.nextWaiter = next; // 否则将trail的后继节点设为next节点。此时,trail节点到next节点中的所有节点被移除(包括t节点,但可能不止t节点。因为,trail始终指向遍历过的最后一个waitStatus为CONDITION,因此只需要将trail的后继节点设置为next,即可将trail之后到next之前的所有节点移除) if (next == null) lastWaiter = trail; } else trail = t; // 如果t的waitStatus为CONDITION,则将trail赋值为t,trail始终指向遍历过的最后一个waitStatus为CONDITION t = next; // t指向下一个节点 } }
- 1、将t赋值为条件队列的尾节点 。
- 2、从t开始遍历整个条件队列。
- 3、如果t的waitStatus不为CONDITION,则断开t与t后继节点的关联。
- 4、如果trail为null,则将firstWaiter赋值为next节点,此时还没有遍历到waitStatus为CONDITION的节点,因此直接移动firstWaiter的指针即可移除前面的节点。
- 5、如果trail不为null,则将trail的后继节点设为next节点。此时,trail节点到next节点中的所有节点被移除(包括t节点,但可能不止t节点。因为,trail始终指向遍历过的最后一个waitStatus为CONDITION,因此只需要将trail的后继节点设置为next,即可将trail之后到next之前的所有节点移除)
- 6、如果t的waitStatus为CONDITION,则将trail赋值为t,trail始终指向遍历过的最后一个waitStatus为CONDITION。
- 7、最后将 t指向下一个节点,准备开始下一次循环。
例子图解过程:
4.4、fullyRelease方法
// 释放锁 final int fullyRelease(Node node) { boolean failed = true; try { // 当前的同步状态 int savedState = getState(); // 独占模式下release(一般指释放锁) if (release(savedState)) { failed = false; return savedState; } else { throw new IllegalMonitorStateException(); } } finally { if (failed) // 如果release失败则将该节点的waitStatus设置为CANCELLED node.waitStatus = Node.CANCELLED; } }
调用release方法(详解见release方法详解)释放锁,如果释放失败,则将该节点的waitStatus设置为CANCELLED。
4.5、isOnSyncQueue方法
// 判断node是否再同步队列中 final boolean isOnSyncQueue(Node node) { if (node.waitStatus == Node.CONDITION || node.prev == null) // 如果waitStatus为CONDITION 或 node没有前驱节点,则必然不在同步队列,直接返回false return false; if (node.next != null) // If has successor, it must be on queue 如果有后继节点,必然是在同步队列中,返回true return true; /* * node.prev can be non-null, but not yet on queue because * the CAS to place it on queue can fail. So we have to * traverse from tail to make sure it actually made it. It * will always be near the tail in calls to this method, and * unless the CAS failed (which is unlikely), it will be * there, so we hardly ever traverse much. */ return findNodeFromTail(node); // 返回node是否为同步队列节点,如果是返回true,否则返回false }
返回node是否为同步队列节点,如果是返回true,否则返回false。因为只有该节点的线程被唤醒(signal())才会从条件队列移到同步队列。
4.6、findNodeFromTail方法
// 从同步队列的尾节点开始向前遍历,如果node为同步队列节点则返回true,否则返回false private boolean findNodeFromTail(Node node) { Node t = tail; for (;;) { if (t == node) return true; if (t == null) return false; t = t.prev; } }
从同步队列的尾节点开始向前遍历,如果node为同步队列节点则返回true,否则返回false。
4.7、signal方法
public final void signal() { // 检查当前线程是否为独占模式同步器的所有者 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) // 唤醒条件队列的头节点 doSignal(first); }
- 1、检查当前线程是否为独占模式同步器的所有者,在ReentrantLock中即检查当前线程是否为拥有锁的线程。如果不是,则抛IllegalMonitorStateException;
- 2、拿到条件队列的头节点,如果不为null,则调用doSignal方法(详解见下文doSignal方法)唤醒头节点。
4.8、doSignal方法
// 将条件队列的头节点移到同步队列 private void doSignal(Node first) { do { if ( (firstWaiter = first.nextWaiter) == null) // 将first节点赋值为first节点的后继节点(相当于移除first节点),如果first节点的后继节点为空,则将lastWaiter赋值为null lastWaiter = null; // 断开first节点对first节点后继节点的关联 first.nextWaiter = null; } while (!transferForSignal(first) && // transferForSignal:将first节点从条件队列移动到同步队列 (first = firstWaiter) != null); // 如果transferForSignal失败,并且first节点不为null,则向下遍历条件队列的节点,直到节点成功移动到同步队列 或者 firstWaiter为null }
- 1、将first节点赋值为first节点的后继节点(相当于移除first节点),如果first节点的后继节点为空,则将lastWaiter赋值为null。
断开first节点与first节点后继节点的关联。 - 2、调用transferForSignal方法(详解见下文transferForSignal方法)将first节点从条件队列移动到同步队列。
- 3、如果transferForSignal失败,并且first节点的后继节点(firstWaiter)不为null,则向下遍历条件队列的节点,直到节点成功移动到同步队列 或者 first节点的后继节点为null。
4.9、transferForSignal方法
// 将node节点从条件队列移动到同步队列,如果成功则返回true。 final boolean transferForSignal(Node node) { /* * If cannot change waitStatus, the node has been cancelled. */ if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) // 如果不能更改节点的waitStatus,则表示该节点已被取消,返回false return false; /* * Splice onto queue and try to set waitStatus of predecessor to * indicate that thread is (probably) waiting. If cancelled or * attempt to set waitStatus fails, wake up to resync (in which * case the waitStatus can be transiently and harmlessly wrong). */ Node p = enq(node); // 否则,调用enq方法将node添加到同步队列,注意:enq方法返回的节点是node的前驱节点 // 将ws赋值为node前驱节点的等待状态 int ws = p.waitStatus; if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) // 如果node前驱节点的状态为CANCELLED(ws>0) 或 使用CAS将waitStatus修改成SIGNAL失败,则代表node的前驱节点无法来唤醒node节点,因此直接调用LockSupport.unpark方法唤醒node节点 LockSupport.unpark(node.thread); return true; }
- 1、如果不能更改节点的waitStatus,则表示该节点已被取消,返回false。
- 2、调用enq方法(详解见enq方法详解)将node添加到同步队列,注意:enq方法返回的节点是node的前驱节点。因此,此时p节点为node的前驱节点。
- 3、将ws赋值为node前驱节点(p节点)的waitStatus。
- 4、如果p节点的waitStatus为CANCELLED(ws>0) 或 使用CAS将p节点的waitStatus修改成SIGNAL失败,则代表p节点无法来唤醒node节点,因此直接调用LockSupport.unpark方法唤醒node节点。
4.10、signalAll方法
public final void signalAll() { // 检查当前线程是否为独占模式同步器的所有者 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) // 唤醒条件队列的所有节点 doSignalAll(first); }
- 1、检查当前线程是否为独占模式同步器的所有者,在ReentrantLock中即检查当前线程是否为拥有锁的线程。如果不是,则抛IllegalMonitorStateException。
- 2、拿到条件队列的头节点,如果不为null,则调用doSignalAll方法(详解见下文doSignalAll方法)唤醒条件队列的所有节点。
4.11、doSignalAll方法
// 将条件队列的所有节点移到同步队列 private void doSignalAll(Node first) { // 因为要移除条件队列的所有节点到同步队列,因此这边直接将firstWaiter和lastWaiter赋值为null lastWaiter = firstWaiter = null; do { // next赋值为first节点的后继节点 Node next = first.nextWaiter; // 断开first节点对first节点后继节点的关联 first.nextWaiter = null; // transferForSignal:将first节点从条件队列移动到同步队列 transferForSignal(first); // first赋值为next节点 first = next; } while (first != null); // 循环遍历,将条件队列的所有节点移动到同步队列 }
- 1、因为要移除条件队列的所有节点到同步队列,因此这边直接将firstWaiter和lastWaiter赋值为null。
- 2、next赋值为first节点的后继节点 。
- 3、断开first节点对first节点后继节点的关联
- 4、调用transferForSignal方法(详解见上文transferForSignal方法)将first节点从条件队列移动到同步队列。
- 5、first赋值为next节点,准备下一次循环。
- 6、如果first不为null,则进入下一次循环。
5、总结
- 1、调用await和signal方法都需要先获得锁,否则会抛异常。
- 2、调用await方法会新建一个waitStatus为CONDITION、线程为当前线程的节点到条件队列尾部,然后当前线程会释放掉锁,并进入阻塞状态,直到该节点被移到同步队列或者被中断。该节点被移动到同步队列,并不代表该节点线程能立马获得锁,还是需要在同步队列中排队并在必要时候(前驱节点为head)调用tryAcquire方法去获取,如果获取成功则代表获得了锁。
- 3、调用signal方法会将条件队列的头节点移动到同步队列。
参考
- AbstractQueuedSynchronizer.ConditionObject源码(JDK 1.8)
- 《Java并发编程的艺术》