聊聊并发:(十一)concurrent包之Condition源码分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wtopps/article/details/84185686

前言

在前几篇文章中,

聊聊并发:(九)concurrent包之ReentrantLock分析

聊聊并发:(八)concurrent包之AbstractQueuedSynchronizer源码实现分析

聊聊并发:(七)concurrent包之AbstractQueuedSynchronizer分析

我们介绍了concurrent包中几种锁的实现机制,对其源码进行了分析,在介绍锁的文章中,并没有提及到Condition这个类,其实Condition的使用是与Lock绑定在一起的,本章,我们详细了解一下Conditon的使用方式以及实现机制。

Condition介绍

想必大家对Object中的三个方法都很熟悉,wait()、notify()以及notifyAll(),使用这几个方法,我们可以实现线程之间的通信,以及一些设计模式的实现,然而使用Object中的这几个方法,是基于对象监视器配合完成线程间的等待/通知机制,在一些场景下往往不是特别的灵活,我们不能控制到更细粒度的级别。

因此,在concurrent包中,Java的为我们提供了Condition类,可以帮助我们更加灵活的实现同样的功能,并且具有更高的可控制性和扩展性。

我们看一下Condition中提供的几个方法:

方法名称 方法描述
void await() 造成当前线程在接到信号或被中断之前一直处于等待状态
boolean await(long time, TimeUnit unit) 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态
long awaitNanos(long nanosTimeout) 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态
void awaitUninterruptibly() 造成当前线程在接到信号之前一直处于等待状态
boolean awaitUntil(Date deadline) 造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态
void signal() 唤醒一个等待线程
void signalAll() 唤醒所有等待线程

Condition提供的方法并不多,其中比较常用的是await()、signal()、signalAll(),分别对应Object中的wait()、notify()、notifyAll()方法,我们下面来分析一下Condition的内部实现机制。

Condition实现机制

我们先来看一下如何创建一个Condition对象:

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

Condition对象的创建,是依赖于Lock中的newCondition()方法,也就是说,Condition的使用是与Lock绑定在一起的,事实上,newCondition()创建的是来自于AQS的ConditionObject对象,该类是AQS的一个内部类,Condition是要和lock配合使用的也就是condition和Lock是绑定在一起的,而lock的实现原理又依赖于AQS,自然而然ConditionObject是AQS的一个内部类

ReentrantLock:

final ConditionObject newCondition() {
    return new ConditionObject();
}

Condition等待队列实现机制

在前面的文章中,我们介绍了AQS的实现机制,其中介绍了AQS实现原理,是内部维护了一个同步队列,condition内部也是使用同样的方式,内部维护了一个等待队列,所有调用condition.await方法的线程会加入到等待队列中,并且线程状态转换为等待状态。

ConditionObject中有两个很重要的变量:

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

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

如果看过AQS实现的朋友可以很熟悉,AQS也采用了同样的实现方式,这里Node类使用的与AQS的Node一致,因此,可以通过firstWaiter、lastWaiter这两个Node对象,去完成一个队列的构建。

ConditionObject内部维护了一个单向的队列,通过Node类进行构建,它的示意图如下:

image

与AQS不同的是,AQS内部维护的是一个双向的队列,而ConditionObject内部维护的是单向的队列。

看到这里,不知道您有没有发现一个问题点,ConditionObject既然是通过调用Lock的newCondition()方法创建出来的,那么,同一个Lock对象,我们调用多次newCondition()方法,会怎么样呢?

事实上,每次调用newCondition()方法,在这个Lock对象上,都会创建出一个等待队列,也就是说,AQS是支持一个锁,同时存在多个等待队列,这个与Object有很大的不同,一个Object的对象监视器上只能拥有一个同步队列和一个等待队列,即仅支持一个线程持有Object的锁后,才可以进入等待状态,而AQS是支持多个线程同时等待一个锁。

我们还是用一张示意图来进行说明:

image

由于ConditionObject是AQS的内部类,可以访问AQS的内部属性,因此可以共享AQS的同步资源的状态,需要注意的一点是,在进入等待队列的先决条件是,当前需要进入等待队列的线程,已经获取到锁,否则会抛出异常。

OK,介绍完ConditionObject的内部结构,接下来我们看一下ConditionObject是如何实现等待机制的。

Condition await()方法实现机制

我们来看一下Condition如何进入等待,这里,我们拿ReentrantLock为例。

Lock lock = new ReentrantLock();
lock.lock();
Condition condition = lock.newCondition();
condition.await();

首先我们需要创建一个锁对象,这里我们创建一个ReentrantLock锁,调用其lock()方法,获取锁资源,然后创建Condition对象,调用其await()方法,此时,当前获取到锁资源的线程,会被挂起,释放掉锁资源,进入等待队列中,等待Condition的signal()信号的唤起。我们进去,看一下await()方法的实现。

AbstractQueuedSynchronizer:

/**
 * 可中断式的condition实现.
 * 1、如果当前线程被打断, 抛出InterruptedException异常.
 * 2、通过getState方法,保存当前同步状态.
 * 3、调用release()方法,将当前同步状态作为入参,如果失败,抛出IllegalMonitorStateException.
 * 4、直到被唤醒或者打断前, 保持阻塞状态.
 * 5、通过调用acquire()方法重新争抢同步资源,使用之前保存的同步状态作为入参.
 * 6、如果当在第四步被打断,抛出InterruptedException异常.
 */
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    //1、新增节点至同步队列
    Node node = addConditionWaiter();
    //2、释放当前线程持有的同步资源,并保持当前同步资源的状态
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //3、如果当前线程不在同步队列中
    while (!isOnSyncQueue(node)) {
        //挂起当前线程
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    //4、自旋等待获取到同步状态(即获取到lock)
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

上面是await()方法的源码实现,大体流程可以参考注释,我们对其中的几个核心点进行分析:

  • 1、新增节点至同步队列
    我们在前面已经说明了ConditionObject内部同步队列的实现机制,await()方法的第一步,就是将当前线程,新增至同步队列的尾部,我们来看一下具体的实现:

    private Node addConditionWaiter() {
        Node t = lastWaiter;
        // If lastWaiter is cancelled, clean out.
        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;
    }
    

    上面的代码就是新增等待队列节点的实现,实现并不复杂,我们简单说一下,首先获取尾部等待节点,如果尾部等待节点不为空并且状态不正确,清除,并重新指定尾部节点,如果状态没问题,新增一个节点加入到队列中,放置到队尾;如果尾部节点为空,证明这是一个空队列,新增一个节点,放置到队尾。

  • 2、释放当前线程持有的同步资源,并保持当前同步资源的状态将当前线程放置到等待队列中后,可以准备释放当前线程持有的同步资源,调用fullyRelease()方法来完成,我们看一下其实现:

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

    这段逻辑比较简单,调用AQS的release()方法,释放掉当前线程持有的同步资源,并唤醒同步队列中的下一个等待节点(具体实现逻辑请参考AQS实现机制一文),如果成功,返回当前线程的同步状态,如果失败,将当前等待节点的状态置为CANCELLED。

    这里需要注意的是,如果当前线程没有持有当前锁的持有者,那么会抛出IllegalMonitorStateException异常。

  • 3、如果当前线程不在同步队列中,挂起线程
    这里,有一个循环,通过isOnSyncQueue()判断,当前节点是否在同步队列中,我们来看一下isOnSyncQueue()的实现:

    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;
        return findNodeFromTail(node);
    }
    

    从上面的实现我们可以知道,当isOnSyncQueue()方法返回false的时候,循环会持续下去,并将当前线程挂起,只有当该方法返回true,或者当前等待节点被打断的时候,循环才会结束。

    我们先来看什么时候isOnSyncQueue()才会返回true,有两种情况:当前节点存在下一个等待节点,或findNodeFromTail()方法返回true,而findNodeFromTail()返回true的情况只有调用了signal()方法的时候,这里我们后面会讲。

    总结一下:
    只有两种情况下,循环再回退出,即当前等待节点被打断,或调用了signal()或signalAll()方法的时候,循环退出,否则,会一直将当前线程挂起。

  • 4、自旋等待获取到同步状态(即获取到lock)
    这里就不过多解释了,会调用AQS的acquireQueued()方法,自旋去尝试获取同步资源。

    对AQS这里的实现不熟悉的朋友,可以参见前面的文章关于AQS原理的介绍。

OK,我们总结一下await()方法,当调用await()后,会讲当前线程放入等待队列中,在调用signal()或signAll()方法前或被打断前,当前线程会一直被挂起。

哈哈,前面啰啰嗦嗦说着这么多,其实核心实现就这么个流程。

Condition signal()与signalAll()方法实现机制

前面分析完了Condition如何进入等待,我们再看一下Condition如何唤醒。

Lock lock = new ReentrantLock();
lock.lock();
Condition condition = lock.newCondition();
condition.await();
condition.signal();

condition的唤醒,需要在await()后执行,同样的,也必须要获取lock后才可以执行,否则会抛出UnsupportedOperationException。

我们来看一下其实现机制:

AbstractQueuedSynchronizer:

public final void signal()方法的逻辑比较简单,可以参见注释,核心方法在于() {
    //1、检测当前线程是否已经持有锁
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //2、获取等待队列的头结点
    Node first = firstWaiter;
    if (first != null)
        //3、唤醒
        doSignal(first);
}

signal()方法的逻辑比较简单,可以参见注释,核心方法在于doSignal(),我们看一下它的实现:

private void doSignal(Node first) {
    do {
        //如果头部等待节点没有下一个节点了 ,将尾部等待节点置为null
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
        //释放
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

这里没有什么可说的,核心在于transferForSignal()方法:

final boolean transferForSignal(Node node) {
    //1、重置节点状态
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    //2、将节点加入到AQS的同步队列中
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

上面的代码是真正的signal()方法的核心逻辑,主要做了两件事情:

  • 1、重置节点状态
  • 2、将节点加入到AQS的同步队列中

该方法会使得等待队列中的头节点即等待时间最长的那个节点移入到同步队列,而移入到同步队列后才有机会使得等待线程被唤醒,还记得我们上面关于await()方法的介绍时,提到过findNodeFromTail()这个方法,其只有节点被移动到同步队列的时候,该方法才会返回true,await()才会退出循环。

signalAll与signal方法的区别体现在doSignalAll方法上,前面我们已经知道doSignal方法只会对等待队列的头节点进行操作,而doSignalAll的源码为:

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

可以看出实现机制基本差不多,区别在于,doSignalAll会循环唤醒等待队列中的全部就节点。

Condition应用场景

Condition最常见的应用场景就是解决“生产者与消费者问题”,假设我们有两个线程,A线程与B线程,当A线程获取到lock锁后,调用await()方法,而B线程,也获取同一个锁后,调用signal()方法,可以使得A线程继续执行,我们用一个小的demo演示一下:

public class ConditionAwaitSignalDemo {

    private static Lock lock = new ReentrantLock();

    private static Condition condition = lock.newCondition();

    private static volatile boolean terminal = false;

    public static void main(String[] args) {
        Thread waiterThread = new Thread(new WaiterThread());
        Thread signalThread = new Thread(new SignalThread());
        waiterThread.start();
        signalThread.start();
    }

    private static class WaiterThread implements Runnable {
        @Override
        public void run() {
            lock.lock();
            try {
                while (!terminal) {
                    System.out.println("等待ing,当前线程:" + Thread.currentThread().getName());
                    condition.await();
                }
                System.out.println("结束等待,当前线程:" + Thread.currentThread().getName());
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    private static class SignalThread implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                terminal = true;
                condition.signal();
            } finally {
                lock.unlock();
            }
        }
    }
}

输出:

等待ing,当前线程:Thread-0
结束等待,当前线程:Thread-0

结语

本篇我们了解了Condition的使用以及实现机制,理解Condition对于我们使用concurrent包中锁会有很大的帮助,本篇介绍的比较粗糙,很多Condition的其他方法没有介绍到,但是其他方法都是大同小异,希望朋友们读完本篇后,也仔细去读一下Condition的源码实现,理解一下其中实现的精髓所在。

谢谢阅读!

下篇预告:concurrent包并发辅助类之CyclicBarrier分析

猜你喜欢

转载自blog.csdn.net/wtopps/article/details/84185686