在前几篇我们聊了 AQS、CLH、ReentrantLock、ReentrantReadWriteLock等的原理以及其源码解读,具体参见专栏《非学无以广才》
这章我们一起聊聊显示的Condition 对象。
在没有Lock之前,我们使用synchronized来控制同步,配合Object的wait()、wait(long timeout)、notify()、以及notifyAll 等方法可以实现等待/通知模式。
Condition接口也提供了类似于Object的监听器方法、与Lock接口配合可以实现等待/通知模式,但是两者还是有很大区别的,下图是两者的对比
参考《Java并发编程艺术》
Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。
条件(也称为条件队列 或条件变量)为线程提供了一个含义,以便在某个状态条件现在可能为 true 的另一个线程通知它之前,一直挂起该线程(即让其“等待”)。因为访问此共享状态信息发生在不同的线程中,所以它必须受保护,因此要将某种形式的锁与该条件相关联。等待提供一个条件的主要属性是:以原子方式 释放相关的锁,并挂起当前线程,就像 Object.wait 做的那样。
Condition 实例实质上被绑定到一个锁上。要为特定 Lock 实例获得 Condition 实例,请使用其 newCondition() 方法。
Condition提供了一系列的方法来对阻塞和唤醒线程:
Condition是一种广义上的条件队列。他为线程提供了一种更为灵活的等待/通知模式,线程在调用await方法后执行挂起操作,直到线程等待的某个条件为真时才会被唤醒。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。
获取一个Condition必须要通过Lock的newCondition()方法。该方法定义在接口Lock下,返回的结果是绑定到此 Lock 实例的新 Condition 实例。
Condition为一个接口,其下仅有一个实现类ConditionObject,由于Condition的操作需要获取相关的锁,而AQS则是同步锁的实现基础,所以ConditionObject则定义为AQS的内部类。
public class ConditionObject implements Condition, java.io.Serializable { // 省略方法 }
每个Condition对象都包含着一个FIFO队列,该队列是Condition对象通知/等待功能的关键。
在队列中每一个节点都包含着一个线程引用,该线程就是在该Condition对象上等待的线程。
我们看Condition的定义就明白了:
public class ConditionObject implements Condition, java.io.Serializable { 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() { } // Internal methods // 省略方法 }
从上面代码可以看出Condition拥有首节点(firstWaiter),尾节点(lastWaiter)。
当前线程调用await()方法,将会以当前线程构造成一个节点(Node),并将节点加入到该队列的尾部。
图来源《Java 并发编程艺术》
Node里面包含了当前线程的引用。Node定义与AQS的CLH同步队列的节点使用的都是同一个类(AbstractQueuedSynchronized.Node静态内部类)。
Condition的队列结构比CLH同步队列的结构简单些,新增过程较为简单只需要将原尾节点的nextWaiter指向新增节点,然后更新lastWaiter即可。
调用Condition的await()方法会使当前线程进入等待状态,同时会加入到Condition等待队列并释放锁。
当从await()方法返回时,当前线程一定是获取了Condition相的锁。
public final void await() throws InterruptedException { // 当前线程中断、直接异常 if (Thread.interrupted()) throw new InterruptedException(); 加入等待 队列 Node node = addConditionWaiter(); //释放锁 int savedState = fullyRelease(node); int interruptMode = 0; //检测当前节点是否在同步队列上、如果不在则说明该节点没有资格竞争锁,继续等待。 while (!isOnSyncQueue(node)) { // 挂起线程 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(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); }
此段代码的逻辑是:
首先将当前线程新建一个节点同时加入到等待队列中,然后释放当前线程持有的同步状态。
然后则是不断检测该节点代表的线程是否出现在CLH同步队列中(收到signal信号之后就会在AQS队列中检测到),如果不存在则一直挂起,否则参与竞争同步状态。
private Node addConditionWaiter() { //队列的尾节点 Node t = lastWaiter; // If lastWaiter is cancelled, clean out. // 如果该节点的状态的不是CONDITION,则说明该节点不在等待队列上,需要 清除 if (t != null && t.waitStatus != Node.CONDITION) { // 清除等待队列中状态部位 CONDITION 的节点 unlinkCancelledWaiters(); //清除后从新获取尾节点 t = lastWaiter; } // 将当前线程构造成等待节点 Node node = new Node(Thread.currentThread(), Node.CONDITION); // 将node 节点添加到等待队列的尾部 if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node; }
该方法主要是将当前线程加入到Condition条件队列中。当然在加入到尾节点之前会清除所有状态不为Condition的节点。
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; } }
isOnSyncQueue(Node node):如果一个节点刚开始在条件队列上,现在在同步队列上获取锁则返回true。
final boolean isOnSyncQueue(Node node) { // 状态为CONDITION 、前驱节点为空,返回false if (node.waitStatus == Node.CONDITION || node.prev == null) return false; // 如果后继节点不为空,则说明节点肯定在同步队列中 if (node.next != null) // If has successor, it must be on queue 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); }
unlinkCancelledWaiters():负责将条件队列中状态不为Condition的节点删除。
private void unlinkCancelledWaiters() { // 首节点 Node t = firstWaiter; Node trail = null; // 从头开始清除状态不为CONDITION的节点 while (t != null) { Node next = t.nextWaiter; if (t.waitStatus != Node.CONDITION) { t.nextWaiter = null; if (trail == null) firstWaiter = next; else trail.nextWaiter = next; if (next == null) lastWaiter = trail; } else trail = t; t = next; } }
调用Condition的signal()方法,将会唤醒在等待队列中等待最长时间的节点(条件队列里的首节点),在唤醒节点前,会将节点移到CLH同步队列中。
public final void signal() { // 如果同步是以独占方式进行的,则返回 true;其他情况则返回 false if (!isHeldExclusively()) throw new IllegalMonitorStateException(); // 唤醒首节点 Node first = firstWaiter; if (first != null) doSignal(first); }
该方法首先会判断当前线程是否已经获得了锁,这是前置条件。然后唤醒条件队列中的头节点。
private void doSignal(Node first) { do { // 修改头节点、方便移除 if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; // 将该节点移到同步队列 } while (!transferForSignal(first) && (first = firstWaiter) != null); }
doSignal(Node first)主要是做两件事:
final boolean transferForSignal(Node node) { /* * If cannot change waitStatus, the node has been cancelled. */ // 将该节点的状态改为初始状态0 if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) 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.prev节点 Node p = enq(node); //如果结点p的状态为cancel 或者修改waitStatus失败,则直接唤醒 int ws = p.waitStatus; if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; }
整个通知的流程如下:
一个线程获取锁后,通过调用Condition的await()方法,会将当前线程先加入到条件队列中,然后释放锁,最后通过isOnSyncQueue(Node node)方法不断自检看节点是否已经在CLH同步队列了,如果是则尝试获取锁,否则一直挂起。
当线程调用signal()方法后,程序首先检查当前线程是否获取了锁,然后通过doSignal(Node first)方法唤醒CLH同步队列的首节点。被唤醒的线程,将从await()方法中的while循环中退出来,然后调用acquireQueued()方法竞争同步状态。
注:本章参考《Java 并发编程艺术》、《Java 并发编程实战》
本人技术有限,如果错误,欢迎拍砖