J.U.C|condition分析

栏目: Java · 发布时间: 7年前

内容简介:在前几篇我们聊了 AQS、CLH、ReentrantLock、ReentrantReadWriteLock等的原理以及其源码解读,具体参见专栏《非学无以广才》这章我们一起聊聊显示的Condition 对象。在没有Lock之前,我们使用synchronized来控制同步,配合Object的wait()、wait(long timeout)、notify()、以及notifyAll 等方法可以实现等待/通知模式。

一、写在前面

在前几篇我们聊了 AQS、CLH、ReentrantLock、ReentrantReadWriteLock等的原理以及其源码解读,具体参见专栏《非学无以广才》

这章我们一起聊聊显示的Condition 对象。

二、简介

在没有Lock之前,我们使用synchronized来控制同步,配合Object的wait()、wait(long timeout)、notify()、以及notifyAll 等方法可以实现等待/通知模式。

Condition接口也提供了类似于Object的监听器方法、与Lock接口配合可以实现等待/通知模式,但是两者还是有很大区别的,下图是两者的对比

J.U.C|condition分析

参考《Java并发编程艺术》

Java API 摘要

Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。

条件(也称为条件队列 或条件变量)为线程提供了一个含义,以便在某个状态条件现在可能为 true 的另一个线程通知它之前,一直挂起该线程(即让其“等待”)。因为访问此共享状态信息发生在不同的线程中,所以它必须受保护,因此要将某种形式的锁与该条件相关联。等待提供一个条件的主要属性是:以原子方式 释放相关的锁,并挂起当前线程,就像 Object.wait 做的那样。

Condition 实例实质上被绑定到一个锁上。要为特定 Lock 实例获得 Condition 实例,请使用其 newCondition() 方法。

三、方法摘要

Condition提供了一系列的方法来对阻塞和唤醒线程:

  1. await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
  2. await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
  3. awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
  4. awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。
  5. awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。
  6. signal() :唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
  7. signalAll() :唤醒所有等待线程。能够从等待方法返回的线程必须获得与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),并将节点加入到该队列的尾部。

J.U.C|condition分析

图来源《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队列中检测到),如果不存在则一直挂起,否则参与竞争同步状态。

加入条件队列(addConditionWaiter())源码如下

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的节点。

fullyRelease(Node 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;
        }
    }

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);
        }

该方法首先会判断当前线程是否已经获得了锁,这是前置条件。然后唤醒条件队列中的头节点。

doSignal(Node 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)主要是做两件事:

  • 修改头节点;
  • 调用transferForSignal(Node first) 方法将节点移动到CLH同步队列中。

transferForSignal(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;
    }

小结

整个通知的流程如下:

  • 判断当前线程是否已经获取了锁,如果没有获取则直接抛出异常,因为获取锁为通知的前置条件。
  • 如果线程已经获取了锁,则将唤醒条件队列的首节点。
  • 唤醒首节点是先将条件队列中的头节点移出,然后调用AQS的enq(Node node)方法将其安全地移到CLH同步队列中 。
  • 最后判断如果该节点的同步状态是否为Cancel,或者修改状态为Signal失败时,则直接调用LockSupport唤醒该节点的线程。

五、总结

一个线程获取锁后,通过调用Condition的await()方法,会将当前线程先加入到条件队列中,然后释放锁,最后通过isOnSyncQueue(Node node)方法不断自检看节点是否已经在CLH同步队列了,如果是则尝试获取锁,否则一直挂起。

当线程调用signal()方法后,程序首先检查当前线程是否获取了锁,然后通过doSignal(Node first)方法唤醒CLH同步队列的首节点。被唤醒的线程,将从await()方法中的while循环中退出来,然后调用acquireQueued()方法竞争同步状态。

注:本章参考《Java 并发编程艺术》、《Java 并发编程实战》

本人技术有限,如果错误,欢迎拍砖


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Probabilistic Method

The Probabilistic Method

Noga Alon、Joel H. Spencer / Wiley-Interscience / 2008-8-11 / USD 137.00

Praise for the Second Edition : "Serious researchers in combinatorics or algorithm design will wish to read the book in its entirety...the book may also be enjoyed on a lighter level since the diffe......一起来看看 《The Probabilistic Method》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具