Condition详解

Condition详解

JDK的Lock是为了替代synchronized同步锁,Condition是为了替代Object的对象监视器锁。

利用Condition,每个对象可以有多个不同的等待集(wait-sets),这样多个线程可以分别在同一个对象的多个等待集上等待,相对于Object的对象监视器锁,Condition提供了更加精细化的锁操作。

Condition(也可以称为Condition queue,条件队列),如果当前线程在某个条件上等待,当该条件满足时,其他线程可以唤醒该线程,该线程可以继续去竞争锁。

Condition和Lock是绑定的,一个Lock可以关联多个Condition,通过Lock.newCondition()方法获得一个Condition。

Condition的使用

举例说明Conditon的使用:

设想我们现在有一个有界的缓存BoundedBuffer,该缓存提供了put和take方法。如果在一个空的BoundedBuffer上take数据,当前线程将会阻塞,直到该缓存有一项数据可用。如果在一个满的BoundedBuffer上put数据,当前线程也会阻塞,直到该缓存有一个空闲位置可用。通过使用两个Condition,可以分别让put和take的线程在不同的条件下等待,这样我们可以优化通知机制,当缓存不空(或者不满)的时候,只通知那些需要take(或者put)数据的线程,避免了无效通知。

看下BoundedBuffer的实现:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * <p>文件描述: 有界缓存</p>
 *
 * @Author luanmousheng
 * @Date 17/8/13 上午10:02
*/
public class BoundedBuffer {

    private final Lock lock = new ReentrantLock();

    //锁lock上的两个条件
    //缓存不满条件
    private final Condition notFull = lock.newCondition();
    //缓存不空条件
    private final  Condition notEmpty = lock.newCondition();

    private final Object[] items = new Object[100];

    private int putptr = 0;
    private int takeptr = 0;
    private int count = 0;

    public void put(Object item) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) {
                //等待缓存不满
                notFull.await();
            }
            items[putptr] = item;
            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 res = items[takeptr];
            if (++takeptr == items.length) {
                takeptr = 0;
            }
            count--;
            //缓存不满,其他添加数据阻塞的线程可以被唤醒了
            notFull.signal();
            return res;
        } finally {
            lock.unlock();
        }
    }
}

Condition接口定义

Condition是一个接口,定义了条件需要实现的语义。

public interface Condition {
    /**
    响应中断的条件等待
    */
    void await() throws InterruptedException;
    /**
    不响应中断的条件等待
    */
    void awaitUninterruptibly();
    /**
    带超时时间的等待,单位:纳秒
    */
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    /**
    带超时时间的等待
    */
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    /**
    带到期时间的等待
    */
    boolean awaitUntil(Date deadline) throws InterruptedException;
    /**
    唤醒某个在该条件上等待的线程
    */
    void signal();
    /**
    唤醒所有在该条件上等待的线程
    */
    void signalAll();
}

Condition的实现

Condition也叫条件队列,AbstractQueuedSynchronizer实现了该接口。以前学习ReentrantLock的时候也说到了AQS,它是JDK锁和并发的核心实现。AQS维护了一个等待队列,如果锁关联了n个Condition,AQS还会维护n个条件队列,队列的节点都是AQS的内部静态类Node。

AQS的内部类ConditionObject实现了Condition接口,看下ConditionObject的定义:

public class ConditionObject implements Condition, java.io.Serializable {
    /**
    条件队列的第一个节点
    */
    private transient Node firstWaiter;
    /**
    条件队列的最后一个节点
    */
    private transient Node lastWaiter;

    //……
}

ConditionObject是通过firstWaiter和lastWaiter维护了一个条件队列。

通常我们会使用条件队列的awaitsignal方法,那我们就首先学习await的实现原理:

await方法

public final void await() throws InterruptedException {
    //await方法通过抛出中断异常响应中断
    if (Thread.interrupted())
      throw new InterruptedException();
    //包装当前线程,加入到条件队列的队尾
    Node node = addConditionWaiter();
    //线程阻塞前释放锁
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //判断该节点是否在等待队列上,如果在等待队列,说明该节点已经被其他线程唤醒并移到了等待队列上
    while (!isOnSyncQueue(node)) {
      //不在等待队列,继续在条件队列阻塞
      LockSupport.park(this);
      //interruptMode!=0,说明在该线程唤醒前或者唤醒后被中断过
      //响应中断,跳出循环
      if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
        break;
    }
    //在等待队列排队等待获取锁
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
      interruptMode = REINTERRUPT;
    //可能存在被取消的节点,删除这些被取消的节点
    if (node.nextWaiter != null) 
      unlinkCancelledWaiters();
    //被中断过,响应中断
    if (interruptMode != 0)
      reportInterruptAfterWait(interruptMode);
}

await方法主要做了这么几件事:

  • 将当前线程加入到条件队列的队尾。
  • 释放当前线程已经获取的锁。
  • 自旋检查当前节点是否在等待队列,如果不是则继续挂起,否则说明有其他线程唤醒了该线程,尝试去等待队列获取锁。
  • 被唤醒或者被中断后,在等待队列中排队获取锁。

await主要流程就是这样,让我们再看下详细实现,看下addConditionWaiter方法:

private Node addConditionWaiter() {
    Node t = lastWaiter;
    //如果条件队列队尾的状态不是CONDITION,可能存在已经取消的节点,删除这些节点
    if (t != null && t.waitStatus != Node.CONDITION) {
      unlinkCancelledWaiters();
      t = lastWaiter;
    }
    //包装当前线程的节点,添加到条件队列队尾
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
      firstWaiter = node;
    else
      t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

fullyRelease释放当前线程持有的锁。

isOnSyncQueue判断该节点是否在等待队列:

final boolean isOnSyncQueue(Node node) {
    //结点状态为CONDITION肯定不在等待队列中
    if (node.waitStatus == Node.CONDITION || node.prev == null)
      return false;
    //存在next节点,说明node肯定在等待队列中
    if (node.next != null) 
      return true;
    //从尾往头遍历查找
    return findNodeFromTail(node);
}

acquireQueued之前在讲ReentrantLock时已经说明过了,这里不再讲解。

signal方法

signal方法唤醒条件队列上某个节点,并将该节点从条件队列移到等待队列。

public final void signal() {
    //当前线程持有锁才能唤醒其他线程,否则抛出监视器异常
    if (!isHeldExclusively())
      throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    //条件队列不为空
    if (first != null)
      //主要逻辑都在这里
      doSignal(first);
}

这断代码的重点是doSignal方法:

private void doSignal(Node first) {
    do {
      if ( (firstWaiter = first.nextWaiter) == null)
        lastWaiter = null;
      first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

doSignal方法遍历条件队列,直到找到第一个未取消的节点。找到第一个未取消的节点后将该节点移到等待队列,然后退出该方法。

看下transferForSignal方法:

//将节点从条件队列移到等待队列
final boolean transferForSignal(Node node) {
    //原子的更新waitStatus为0
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
      return false;
    //将该节点添加到等待队列
    //返回当前节点的前置节点
    Node p = enq(node);
    int ws = p.waitStatus;
    //如果前置节点已取消或者设置状态为SIGNAL失败,则唤醒当前节点线程
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
      LockSupport.unpark(node.thread);
    return true;
}

该方法将节点从条件队列移到等待队列。

signalAll方法

该方法唤醒条件队列中的所有节点线程,并将这些节点从条件队列移到等待队列。

public final void signalAll() {
    if (!isHeldExclusively())
      throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
      doSignalAll(first);
}

主要看下doSignalAll方法:

//将first及其之后的节点从条件队列移到等待队列
private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
      Node next = first.nextWaiter;
      //GC
      first.nextWaiter = null;
      //将节点移到等待队列
      transferForSignal(first);
      first = next;
      //当节点不为空时,转移节点
    } while (first != null);
}

这断代码逻辑比较简单,遍历条件队列,调用transferForSignal方法,将节点从条件队列移到等待队列。

以上就是条件队列的主要逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值