Condition浅析

背景

我们在谈到多线程并发访问临界资源的时候,通常会想到Object类的monitor锁和wait/notify(条件等待/通知)操作。很多同学会想问,既然有了加锁/解锁机制,为什么还要引入条件等待/通知操作呢?原因在于,当A线程对临界资源加锁后(排他锁),其他线程将无法再次获取该临界资源,直到A线程将锁释放。如果A线程执行很久,那么其他线程将在很长一段时间内无法持有临界资源的锁。但是对于Object.wait,A线程将放弃锁,进入等待池,等其他线程调用Object.notifyAll的时候,对它进行唤醒,然后再次竞争锁。因此,在实际工程使用中,如果A线程持有锁之后,需要等待其他条件时,通常会采用将其加入条件等待队列的方式,放弃锁,等待其他线程通知它恢复运行。

锁与条件等待机制的实现方式

锁与条件等待机制的实现方式主要有两种:基于Object的方式和基于ReentrantLock的方式。

基于Object的方式

这种方式下,是围绕Object类展开的。

使用synchronized关键字,本质上是使用Object的monitor锁提供锁的机制。

使用Object的wait和notify/notifyAll方法,通过JVM底层实现了条件等待队列和通知机制。

基于可重入锁的方式

这种方式主要是围绕着ReentrantLock展开的。

在之前的文章中我们简单介绍了可重入锁:ReentrantLock,其可重入性解决synchronized不可重入的问题,其采用state状态来维护是否有线程持有锁,采用线程同步队列来将未获取锁的线程进行排队,对于获取锁的操作,提供了公平和非公平两种方式。此外,它还提供了带超时机制的获取锁的方式(tryLock带有超时时间),以及非阻塞地方式尝试获取锁(无参数tryLock),支持了更多获取锁的场景。

而其条件等待与条件通知的机制是基于Condition来实现的。本文中将重点介绍一下Condition与ReentrantLock的关系,以及它是如何实现条件等待与条件通知机制的。

Condition

Condition其实是一个接口,其比较重要的方法为await系列方法(无超时时间,有超时时间...)和signal系列方法(signal单个线程和signal全部线程)。

正如Object的wait/notify机制是围绕着Object对象那样,ReentrantLock通过newCondition方法创建ConditionObject,这是AQS(AbstractQueuedSynchronizer)中的一个内置类。

下边我们来依次介绍一下await方法和signal方法。这是两个主干方法,基于类似的思路可以实现带超时时间的await方法,通知全部条件等待线程的signalAll方法。

await方法

await方法执行流程如下,大部分操作上我都进行了标注。

 public final void await() throws InterruptedException {
            // 如果线程已被中断,则抛中断异常
            if (Thread.interrupted())
                throw new InterruptedException();
            // 将本线程包装成节点,加入到条件等待队列的队尾
            // 在该方法中,先判断队尾节点是否存在(即队列中已有等待节点),如果节点已经存在
            // 但节点不是Node.CONDITION状态,则说明该节点对应的线程已经被CANCEL,去除这些
            // CANCEL掉的节点,从后往前找到Node.CONDITION状态的节点,排在它后边,如果没有
            // 这样的节点,那么本节点就是条件等待队列中的头节点
            Node node = addConditionWaiter();
            // 释放锁,由于await方法执行的时候一定是持有锁,所以其释放掉锁之后,线程同步队列
            // 中的其他线程就可以尝试获取锁了
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 这里我们稍后分析一下
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 参考ReentrantLock.lock方法,执行到这里说明该线程已经被唤醒了,
            // 此时它将可以去尝试获取锁了,获取到就可以继续向下执行,反之则park
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

下边来看一下while循环。Node的prev和next是指其在线程同步队列上的前置节点与后继节点;Node在条件等待队列的后置节点为nextWaiter。所以检查Node的prev和next就能知道节点是否已经在同步队列上了。

final boolean isOnSyncQueue(Node node) {
        // 如果waitStatus为Node.CONDITION,那么一定不在线程同步队列中
        // 如果节点没有前置节点了,那么已经被唤醒了
        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);
    }

signal方法

该方法用于将条件等待队列中排在第一位的线程唤醒,加入到线程同步队列中,去尝试获取锁,调用链如下所示。

 public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }
 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) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        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 p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

首先获取条件等待队列中的第一个节点,通过CAS操作将其waitStatus变成0,然后加入到线程同步队列中,检查其前置节点的状态(因为前置节点的状态对当前节点后续行为影响很大)。如果其前置节点已经被CANCEL了,或者设置SIGNAL状态失败,则说明轮到当前节点对应的线程获取锁了,将该线程唤醒。

实际使用场景

锁与条件等待/唤醒机制在实际工程应用中使用得非常频繁,所以涉及到多线程并发、线程挂起/恢复的场景通常都会对此有所涉及。在这里,我们举一个非常简单的例子加以说明。

关注JDK的ArrayBlockingQueue的实现,在这里我们只选取了能体现ReentrantLock和Condition的代码片段。我们关注put/take方法,这两个方法采用阻塞的方式从blockingQueue中添加/获取元素,所谓阻塞方式是指,当队列满时,put操作将被阻塞;当队列空时,take操作将被阻塞。那么阻塞操作是如何实现的呢?对队列(临界资源)的线程安全性又是如何实现的呢?让我们来看一下代码。

首先ArrayBlockingQueue中带有lock,每次进行put/take操作的时候,会先尝试获取锁;操作完毕后会释放锁。

另外,关注到两个Condition,分别是notEmpty和notFull,其在ArrayBlockingQueue的构造函数中,被赋值为lock.newCondition,也就是说这两个Condition都是基于lock的。

当put的时候,首先尝试获取锁,然后判断是否队列已满,满的话notFull.await,将线程挂起,等待队列不满的时候唤醒。

当take的时候,同样尝试获取锁,然后判断队列是否已空,空的话notEmpty.wait,将线程挂起,等待队列中有元素的时候唤醒。

初始状态下,队列为空,take操作被阻塞,当put操作进行时,会在将元素入队之后,调用notEmpty.signal,唤醒被挂起的take线程,使得挂起的take操作的线程开始工作;假设生产者速度远远大于消费者速度,当队列中元素已满时,put操作的线程被挂起,等待take操作通过消费队列中的数据,调用notFull.signal,唤醒挂起的执行put操作的线程。这两个锁相互作用,完成阻塞队列的功能。

/** Main lock guarding all access */
    final ReentrantLock lock;

    /** Condition for waiting takes */
    private final Condition notEmpty;

    /** Condition for waiting puts */
    private final Condition notFull;

public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal();
    }

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值