Condition接口实现

概述

Object 方法Condition 方法区别
void wait()void await()
void wait(long timeout)long awaitNanos(long nanosTimeout)时间单位,返回值
void wait(long timeout, int nanos)boolean await(long time, TimeUnit unit)时间单位,参数类型,返回值
void notify()void signal()
void notifyAll()void signalAll()
-void awaitUninterruptibly()Condition独有
-boolean awaitUntil(Date deadline)Condition独有

这里先做一下说明,本文说wait方法时,是泛指wait()、wait(long timeout)、wait(long timeout, int nanos) 三个方法,当需要指明某个特定的方法时,会带上相应的参数。同样的,说notify方法时,也是泛指notify(),notifyAll()方法,await方法和signal方法以此类推。

首先,我们通过wait/notify机制来类比await/signal机制:

  • 调用wait方法的线程首先必须是已经进入了同步代码块,即已经获取了监视器锁;与之类似,调用await方法的线程首先必须获取lock锁
  • 调用awair方法的线程会释放已经获得的监视器锁,进入当前监视器的等待队列(wait set)中;与之类似,调用await方法的线程会释放已经获取的lock锁,进入到当前Condition对应的条件队列中。
  • 调用监视器锁的notify方法会唤醒等待在该监视器锁上的线程,这些线程将开始参与锁竞争,并在获得锁之后,从wait方法处恢复执行;与之类似,调用Condition的signal方法会唤醒对应的条件队列中的线程,这些线程将开始参与锁竞争,并在获得锁后,从await方法出开始恢复执行。

实现

class BoundBuffer {
	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();	//数组已满,没有空间时,挂起等待,直到数组“非满”(not full)
				
			items[putptr] = x;
			if (++putptr == items.length) 
				++count;
			//因为放入了哟个数据,数组肯定不是空的了
			//此时唤醒等待这notEmpty条件上的线程
			notEmpty.singnal();	
	} finally {
		lock.unlock();
	}
	// 消费者方法,从数组里面拿数据
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await(); // 数组是空的,没有数据可拿时,挂起等待,直到数组非空(notEmpty)
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            // 因为拿出了一个数据,数组肯定不是满的了
            // 此时唤醒等待这notFull条件上的线程
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

一个典型的生产者-消费者模型。这里在同一个lock锁上,创建了两个条件队列notFull, notEmpty。当数组已满,没有存储空间时,put方法在notFull条件上等待,直到数组“not full”;当数组空了,没有数据可读时,take方法在notEmpty条件上等待,直到数组“not empty”,而notEmpty.signal()和notFull.signal()则用来唤醒等待在这个条件上的线程。

注意,上面所说的,在notFull notEmpty条件上等待事实上是指线程在条件队列(condition queue)上等待,当该线程被相应的signal方法唤醒后,将进入到sync queue中去争锁,争到锁后才能在await方法出返回。这里牵涉到两种队列-----condition queue和sync queue,他们都定义在AQS中。

同步队列 VS 条件队列

sync queue

首先,所有等待锁的线程都会被包装Node仍扔到一个同步队列中。该同步队列如下:
在这里插入图片描述
sync queue是一个双向链表,我们使用prev、next属性来串联节点。但是在这个同步队列中,我们一直没有用到nextWaiter属性,即使实在共享锁模式下,这一属性也只作为一个标记,指向了一个空节点,因此,在sync queue中,我们不会用它来串联节点。

condition queue

每创建一个Condition对象就会对应一个Condition队列,每一个调用Condition对象的await方法的线程都会被包装成Node扔进一个条件队列中,就像这样:
在这里插入图片描述
可见,每一个Condition对象对应一个Condition队列,每个Condition队列都是独立的,互不影响的。在上图中,如果我们对于线程调用了notFull.await(),则当前线程就会被包装成Node加到NotFull队列的末尾。

值得注意的时,condition queue是一个单向链表,在该链表中我们使用nextWaiter属性来串联链表。但是就像在sync queue中不会用nextWaiter属性来串联链表一样,在condition queue中,也并不会用到prev、
next属性,它们的值都为null。也就是说,在条件队列中,Node节点真正用到的属性只有三个:

  • thread::代表当前正在等待某个条件的线程
  • waitStatus:条件的等待状态
  • nextWaiter;指向条件队列中的下一个节点
volatile int waitStatus;
static final int CANCELLED =  1;
static final int SIGNAL    = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;

在条件队列里,我们只需要关注一个值即可-------CONDITION。它表示线程处于正常的等待状态,而只要waitStatus不是CONDOTION,我们就认为线程不在等待,此时就要从等待队列中出队。

sync queue 和 condition queue 的联系

一般情况下,等待锁的sync queue和条件队列condition queue时相互独立的,彼此之间并没有任何关系。但是,当我们调用某个条件队列的signal方法,会将某个或所有等待这个条件队列中的线程唤醒,被唤醒的线程和普通线程一样需要去竞争,如果没有抢到,则同样需要被加到等待锁的sync queue中去,此时节点就从condition queue中转移到sync queue中:
在这里插入图片描述
但是,这里尤其要注意的是,node是被一个一个转移过去的,哪怕我们调用的是signalAll()方法也是一个一个转移过去的,而不是整个条件队列街道sync queue的末尾。

同时要注意的是,我们在sync queue中只使用prev、next来串联链表,而不使用nextWaiter;我们在condition queue中只使用nextWaiter来串联链表,而不使用prev、next。事实上,它们就是两个使用了同样的Node数据结构的完全独立的两个链表。因此将节点从condition queue中转移到sync queue中时,我们需要段俩原来的链接(nextWaiter),建立新的链接(prev、next),在某种程度上也是需要将节点一个一个地转移过去地原因之一。

入队时和出队时地锁状态

sync queue是等待锁的队列,当一个线程被包装成Node加到该队列中时,必然是没有获取锁;当该处于该队列的节点获取到了锁,他将从该队列中移除(事实上移除操作是将获取到锁的节点设为新的dummy head,并将thread属性置为null)。

condition queue是等待在特定条件下的队列,因为调用await方法时,必然时已经获得了lock锁,所以在进入condition队列前线程必然是已经获取了锁;在被包装成Node扔进条件队列后,线程将释放锁,任何挂起;当处于该队列的线程被signal方法唤醒后,由于队列中的节点在之前挂起的时候已经释放锁了,所以必须先去再次竞争锁,因此,该节点会添加到sync queue中。因此,条件队列在出队时,线程并不持有锁。

所以事实上,这两个队列的锁状态正好相反:

  • condition queue:入队已经持有锁 --> 在队列中释放锁 --> 离开队列时没有锁 --> 转移到sync queue
  • sync queue:入队没有锁 --> 在队列中竞争锁 --> 离开队列时获取到了锁

ConditionObject

AQS对Condition这个接口的实现主要是通过ConditionObject,上面已经说了,它的核心实现就是一个条件队列,每一个在某个condition上等待的线程都会被封装成Node对象扔进这个条件队列。

核心属性
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;

这两个属性分别代表了条件队列的对头和队尾,每当我们新建一个conditionObject对象,都会对应一个条件队列

Condition接口方法实现

await()第一部分分析
public final void await() throws InterruptedException {
    // 如果当前线程在调动await()方法前已经被中断了,则直接抛出InterruptedException
    if (Thread.interrupted())
        throw new InterruptedException();
    // 将当前线程封装成Node添加到条件队列
    Node node = addConditionWaiter();
    // 释放当前线程所占用的锁,保存当前的锁状态
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 如果当前队列不在同步队列中,说明刚刚被await, 还没有人调用signal方法,则直接将当前线程挂起
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this); // 线程将在这里被挂起,停止运行
        // 能执行到这里说明要么是signal方法被调用了,要么是线程被中断了
        // 所以检查下线程被唤醒的原因,如果是因为中断被唤醒,则跳出while循环
        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);
    */
}

首先是将当前线程封装成Node扔到条件队列中的addConditionWaiter方法:

addConditionWaiter
/**
 * Adds a new waiter to wait queue.
 * @return its new wait node
 */
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // 如果尾节点被cancel了,则先遍历整个链表,清除所有被cancel的节点
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // 将当前线程包装成Node扔进条件队列
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    /*
    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
    */
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

首先我们要思考的是,存在两个不同的线程同时入队的情况吗?不存在,因为能够调用await方法的线程必然是已经获取到了锁的,而获取到了锁的线程只有一个,索引这里不存在并发,因此不需要CAS操作。

在这个方法中,我就是简单的将当前线程封装成Node加到条件队列的末尾。这和将一个线程封装成Node加入等待队列略有不同:

  1. 节点加入sync queue是waitStatus的值为0,但节点加入condition queue时waitStatus的值为Node.CONDITION
  2. sync queue的头节点为dummy节点,如果队列为空,则会先创建一个dummy节点,初始化时,直接将firstWaiter和lastWaiter直接指向新建的节点就可以了。
  3. sync queue是一个双向队列,在节点入队后,要同时修改当前节点的前驱和前驱节点的后继;而在condition queue中,我们只修改了前驱节点的nextWaiter,也就是说,condition queue是作为单向队列来使用的。
    如果入队时发现尾节点已经取消等待了,那么我们就不应该接在它的后面,此时需要调用unlinkCancelledWaiters来提出那些以今取消等待的线程:
private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;
    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;
    }
}

该方法将从头节点开始遍历整个队列,剔除其中waitStatus不为Node.CONDTION的节点,这里使用了两个指针firstWaiter和trail来分别记录第一个和最后一个waitStatus不为Node.CONDTION的节点,这些都是基础的链表操作,很容易理解.

fullyRelease

再借点被成功添加到队列的末尾后,我们将调用fullyRelease来释放当前线程所占用的锁:

/**
 * Invokes release with current state value; returns saved state.
 * Cancels node and throws exception on failure.
 * @param node the condition node for this wait
 * @return previous sync state
 */
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;
    }
}

首先,当我们嗲用这个方法时,说明当前线程已经被封装成Node扔进条件队列了。在该方法中,我们通过release方法,释放锁。

值得注意的是,这是一次性释放了所有的锁,即对可重入锁而言,无论重入几次,这里是一次性释放完的。但是要注意**release(savedState)**方法又可能抛出IllegalMonitorStateException的,这是因为当前线程肯并不是持有锁的线程,但是之前不是说,只有持有锁的线程才能调用aeait方法吗?既然fullyRelease方法在await方法中,为啥当前线程还有可能并不是持有锁的线程呢?

因为在调用await方法时,我们其实并没有检测Thread.currentThread() == getExclusiveOwnerThread(),换句话说,也就是执行到fullyRelease这一步,我们才会检测这一点,而这一点检测是由AQS的子类实现tryRelease方法来保证的,例如:ReentrantLock对tryRelease方法的实现如下:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

当发现当前线程不是持有锁的线程时,我们就会进入finally块,将当前Noded额状态设置为Node.CANCELLED,这也就是为什么上面的addConditionWaiter在添加新节点钱每次都会检测尾节点是否已经被取消了。

在当前线程的锁被完成释放之后,我们能就可以调用LockSupport.park(this)把当前线程挂起,等待被signal了。但是,在挂起当前线程之前我们先用isOnSyncQueue确保了它不在sync queue中,这是为什么呢?当前线程不是在一个和sync queue无关的条件队列中吗?为什么可能会出现在sync queue中的情?

/**
 * Returns true if a node, always one that was initially placed on
 * a condition queue, is now waiting to reacquire on sync queue.
 * @param node the node
 * @return true if is reacquiring
 */
final boolean isOnSyncQueue(Node node) {
    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);
}
/**
 * Returns true if node is on sync queue by searching backwards from tail.
 * Called only when needed by isOnSyncQueue.
 * @return true if present
 */
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

为了解释这一问题,我们先来看看signal方法

signalAll()

在看signalAll之前,我们首先要区分调用signalAll方法的线程进入与signal方法要唤醒的线程(等待在对应的条件队列里的线程):

  • 调用signalAll方法的线程本身是已经持有了锁,现在准被释放锁了;
  • 在条件队列里的线程已经在对应的条件上挂起了,等待这被signal唤醒,然后去争锁。

首先,与调用notify线程必须持有来监视器锁类似,在调用condition的signal方法时,线程也必须是已经持有了lock锁:

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

该方法首先检查当前调用signal方法的线程是不是持有锁的线程,这是通过isHeldExclusively方法来实现的,该方法是由继承AQS的子类来实现的,例如,ReentrantLock对该方法的实现为:

protected final boolean isHeldExclusively() {
    return getExclusiveOwnerThread() == Thread.currentThread();
}

因为exclusiveOwnerThread保存了,当前持有锁的线程,这里只要检测它是不是等于当前线程就行了。
接下来先通过firstWaiter是否为空判断条件队列是否为空,如果条件队列不为空,则调用doSignalAll方法:

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}

首先我们通过lastWaiter = firstWaiter = null;将整个条件队列清空,然后通过一个do-while循环,将原先的条件队列里面的节点一个一个拿出来(令nextWaiter = null),再通过transferForSignal方法一个一个添加到sync queue的末尾

/**
 * Transfers a node from a condition queue onto sync queue.
 * Returns true if successful.
 * @param node the node
 * @return true if successfully transferred (else the node was
 * cancelled before signal)
 */
final boolean transferForSignal(Node node) {
    // 如果该节点在调用signal方法前已经被取消了,则直接跳过这个节点
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    // 如果该节点在条件队列中正常等待,则利用enq方法将该节点添加至sync queue队列的尾部
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread); 
    return true;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值