Condition学习

Condition接口是在java 1.5中才出现的,位于java.util.concurrent.locks包下。

它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition。Lock替换synchronized方法和语句的使用, Condition取代了对象监视器方法的使用。

以生产者与消费者案例举例对比两种的共同与差异:

  1. Object的wait()、notify()必须保证生产者与消费者使用的监视器对象是同一个,Lock与Condition组合需保证锁对象是同一个lock实例
  2. Object的notify()无法区分生产者或消费者,所以存在唤醒同类的情况,在多生差多消费者模式下可能出现假死(过多的同类唤醒同类),所以需要使用notifyAll。
  3. Condition可以通过使用不同的condition实例对象对生产者消费者进行分类,从而达到指定某一类休眠或唤醒。

使用对象监视器方法必须保证:

  1. Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() ,每一个condition示例唯一对应一个lock实例,一个lock可以生产多个condition
  2. 调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用

等待队列是有序的FIFO先进先出队列,调用signal()方法是按顺序从一个头节点开始“激活”,如果失败的话才会依次继续尝试第二个,默认激活等待时间最长的也就是队列的最前端(这里激活不是真正的激活而只是将节点加入到同步阻塞队列尾部),加入到同步阻塞队列。也就是说先等待的先进入同步阻塞队列(这和传统的Object中的  wait/notify机制不太一样,notify方法是随机唤醒一个),但是真正的激活线程是由lock.unlock()去处理。


jdk源码中的演示demo:

class BoundedBuffer {
    final Lock lock = new ReentrantLock();
    final Condition notFull = lock.newCondition();
    final Condition notEmpty = lock.newCondition();

    final Object[] items = new Object[100];
    int putptr, takeptr, count;

    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();
            items[putptr] = x;
            if (++putptr == items.length)
                putptr = 0;
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();
            Object x = items[takeptr];
            if (++takeptr == items.length)
                takeptr = 0;
            --count;
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

Condition接口

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


Condition的优势

对于等待/通知机制,简化而言,就是等待一个条件,当条件不满足时,就进入等待,等条件满足时,就通知等待的线程开始执行。为了实现这种功能,需要进行wait的代码部分与需要进行通知的代码部分必须放在同一个对象监视器里面执行,才能实现多个阻塞的线程同步执行代码,等待与通知的线程也是同步进行。对于wait/notify而言,对象监视器与等待条件结合在一起 即synchronized(对象)利用该对象去调用wait以及notify。但是对于Condition类,是对象监视器与条件分开,Lock类来实现对象监视器,condition对象来负责条件,去调用await以及signal。而且一个Lock可以创建多个不同的Condition实例(也就意味着有多个等待队列,但是同步阻塞队列是唯一的),一个Condition实例可以在不同时机不同条件下执行等待唤醒操作。


Condition的实现分析

1. 属性

我们先看下ConditionObject中的属性:

    /** First node of condition queue. */
    private transient Node firstWaiter;
    /** Last node of condition queue. */
    private transient Node lastWaiter;

每个条件变量都维护了一个容器,ConditionObject中的容器就是单向链表队列,上面的属性就是队列的头结点firstWaiter和尾结点lastWaiter,需要注意,条件队列中的头结点不是虚拟头结点,而是包装了等待线程的节点!其类型和同步队列一样,也是使用AQS的内部类Node来构成,但与同步队列不同的是,条件队列是一个单向链表,所以他并没有使用Node类中的next属性来关联后继Node,而使用的nextWaiter

这里我们需要注意,nextWaiter是没用volatile修饰的,为什么呢?因为线程在调用await方法进入条件队列时,是已经拥有了锁的,此时是不存在竞争的情况,所以无需通过volatile和cas来保证线程安全。而进入同步队列的都是抢锁失败的,所以肯定是没有锁的,故要考虑线程安全

最后需要注意一点的是,条件队列里面的Node只会存在CANCELLED和CONDITION的状态

2. 等待队列

ConditionObject是AQS的内部类,它实现了Condition接口。每个Condition对象都包含一个条件队列(等待队列)。等待队列是一个FIFO(first in first out)的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会构造成节点加入等待队列并进入等待状态,然后释放锁。AQS有一个同步队列和多个等待队列,节点都是Node。等待队列的基本结构如下所示。

等待队列分为首节点和尾节点。当一个线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列。新增节点就是将尾部节点指向新增的节点。节点引用更新本来就是在获取锁以后,释放锁之前的的操作,所以不需要CAS保证。同时也是线程安全的操作。

 如果从队列(同步队列和等待队列)的角度去看await()方法,当调用await()方法时,相当于同步队列的首节点(获取锁的节点)移动到Condition的等待队列中。

成功的获取锁的线程(也就是同步队列的首节点)调用await()方法,该方法会将当前线程重新构造成节点并加入到等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。

2. 源码分析

或者参考文章AQS的ConditionObject源码详解 - 简书

2.1 等待

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;
        // 清除被激活的节点,延迟执行(参考addConditionWaiter方法)
      if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
        //如果当前线程被中断,处理中断逻辑
      if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
    }


主要分以下几步
    (1)先判断是否当前线程是否被中断中断则抛出中断异常如果未中断调用addConditionWaiter()加入等
待队列
    (2)调用fullyRelease(node)释放锁使同步阻塞队列的下个节点线程能获取锁。
    (3)调用isOnSyncQueue(node)判断是否在同步阻塞队列,这里的加入同步阻塞队列操作是在另一个线
程调用signal()后加入,如果不在同步阻塞队列会进行阻塞直到被激活。
    (4)如果被激活然后调用checkInterruptWhileWaiting(node)判断是否被中断并获取中断模式。
    (5)继续调用isOnSyncQueue(node)判断是否在同步阻塞队列。
    (6)是则调用acquireQueued(node, savedState) 获取锁,获取不到,则会阻塞。关于此方法,可参考lock学习笔记02中的源码分析。acquireQueued(node, savedState)也会返回当前线程是否被中断,如果
被中断设置中断模式。
    (7)在激活后调用unlinkCancelledWaiters()清理等待队列的已经被激活的节点。
    (8)最后判断当前线程是否被中断,如果被中断则对中断线程做处理。
private Node addConditionWaiter() {
        //获取等待队列尾部节点
      Node t = lastWaiter;
      //如果尾部状态不为CONDITION,如果已经被"激活",清理之,然后重新获取尾部节点
      if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
      }
        //创建以当前线程为基础的节点,并将节点模式设置成CONDITION
      Node node = new Node(Thread.currentThread(), Node.CONDITION);
        //如果尾节点不存在,说明队列为空,将头节点设置成当前节点
      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;
    }
  }

调用getState()先获取阻塞队列中当前线程节点的锁状态值,这个值可能大于1表示多次重入,然后调用 
release(savedState)释放所有锁,如果释放成功返回锁状态值
(这里需要先熟悉一下Node的结构,可以参考一下Node笔记)
final boolean isOnSyncQueue(Node node) {
        //判断当前节点是否是CONDITION或者前置节点是否为空如果为空直接返回false
    if (node.waitStatus == Node.CONDITION || node.prev == null)
      return false;
    //如果下个节点存在,则在同步阻塞队列中,返回true
    if (node.next != null) // If has successor, it must be on queue
      return true;
        //遍历查找当前节点是否在同步阻塞队列中
    return findNodeFromTail(node);
  }
  private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
      if (t == node)
        return true;
      if (t == null)
        return false;
      t = t.prev;
    }
  }

此方法的功能是查找当前节点是否在同步阻塞队列中,方法先是快速判断,判断不了再进行遍历查找。

(1)第一步先判断次节点是否CONDITION状态或者前置节点是否存在,如果是表明不在队列中返回false,阻
塞队列中的状态一般是0或者SIGNAL状态,而且如果当前节点在阻塞队列中且未被激活,则前置节点一定不为空
(因为阻塞队列中的第一个节点(只有它的前置节点能为null)是一个无意义的空节点)。
(2)第二步判断节点的下个节点是否存在,如果存在则表明当前当前节点已加入到阻塞队列中。
(3)如果以上2点都没法判断,也有可能刚刚加入到同步阻塞队列中,所以调用findNodeFromTail(Node 
node)做最后的遍历查找。查找从队列尾部开始查,从尾部开始查的原因是可能刚刚加入到同步阻塞队列中,从
尾部能快速定位。
private int checkInterruptWhileWaiting(Node node) {
      return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
    }
 
final boolean transferAfterCancelledWait(Node node) {
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
      enq(node);
      return true;
    }
    while (!isOnSyncQueue(node))
      Thread.yield();
    return false;
  }

此方法在线程被激活后被调用,主要功能就是判断被激活的线程是否被中断。此方法会返回2种中断状态THROW_IE
和REINTERRUPT,THROW_IE是调用signal()前被中断返回,REINTERRUPT在调用signal()后被中断返回。 此方
法先判断是否被标记中断,是的话再调用transferAfterCancelledWait(node)取判断是那种中断状态,
transferAfterCancelledWait(node)方法分2步:

    (1)用CAS方式将节点状态从CONDITION改成0,并加入到同步阻塞队列中返回true
    (2)如果不能加入到同步阻塞队列就自旋一直等待加入

如果使用await()方法上面2步其实是没什么作用其最后一定会返回false,因为await()被激活只能调用 
signal()方法,而signal()方法肯定已经将节点加入到同步阻塞队列中。所以以上逻辑是给await(long time,
TimeUnit unit)等待超时激活方法用的。
private void unlinkCancelledWaiters() {
		//获取等待队列头节点
            Node t = firstWaiter;
            Node trail = null;
            while (t != null) {
		//获取下个节点
                Node next = t.nextWaiter;
		//如果状态不为CONDITION说明已经加入阻塞队列需要清理掉
                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的节点并清理,状态不为CONDITION节点说明此节点已经加入到阻塞队列,已经不需要维护
private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
		//如果是THROW_IE模式直接抛出异常
            if (interruptMode == THROW_IE)
                throw new InterruptedException();
		//如果是REINTERRUPT模式标记线程中断由上层处理中断
            else if (interruptMode == REINTERRUPT)
                selfInterrupt();
        }

2.2 唤醒

signal()方法源码如下

public final void signal() {
		//是否当前线程持有锁
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
		//通知"激活"头节点线程
            if (first != null)
                doSignal(first);
        }

先调用isHeldExclusively()判断锁是否被当前线程持有,然后检查等待队列是否为空,不为空就是可以取第一
个节点调用doSignal(first)去"激活",这里激活不是真正的激活而只是将节点加入到同步阻塞队列尾部,所以上
下文中带""的激活都是这种解释。
protected final boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

实现就是比较下当前线程和持有锁的线程是否同一个
        private void doSignal(Node first) {
            do {
		//头指头后移一位,如果后面的节点为空,则将尾指头也指向空,说明队列为空了
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
		//清空头节点的下个节点
                first.nextWaiter = null;
		//如果"激活"失败者取下个继续,直到成功或者遍历完
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

此方法就是取当前头节点一直去尝试"激活",直到成功或者遍历完。
final boolean transferForSignal(Node node) {
		//将CONDITION状态设置成0
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
		//加入到同步阻塞队列,这里返回值p是node的前驱节点
        Node p = enq(node);
        int ws = p.waitStatus;
		//状态异常直接激活
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }


    (1)此方法先先将CONDITION状态设置成0,因为如果是CONDITION状态加入到同步阻塞队列,激活的时候是
不识别的。
    (2)加入到同步阻塞队列的尾部。所以同步阻塞队列中前面如果有多个在排队,调用unlock()不会马上激活
此节点。
    (3)状态异常直接调用unpark激活,这边按理说如果状态异常情况下激活,await()在调用unlock()被激
活后会进行相应的异常处理,但看await()代码没有处理则是正常执行。

这个方法主要就是把节点加入到同步阻塞队列的,真正的激活则是调用unlock()去处理。同步阻塞队列中的节点如何争抢锁取决于lock(公平锁还是非公平锁)参考lock笔记

AQS的同步队列与Condition的等待队列,两个队列的作用不同,事实上,每个线程也仅仅会同时存在于以上两个队列中的一个

推荐参考文章Condition源码分析 - 哈哈呵h - 博客园

java Condition源码分析_Code-lover's Learning Notes-CSDN博客_condition源码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值