阅读 194

重入锁的好搭档:Condition

介绍

任意Java对象都拥有一组监视器方法定义在java.lang.Object方法上,主要包括:

//等待
public final void wait() throws InterruptedException
//超时限制等待
public final native void wait(long timeout) throws InterruptedException
//超时限制等待
public final void wait(long timeout, int nanos) throws InterruptedException
//通知某个线程
public final native void notify()
//通知所有等待线程
public final native void notifyAll()

复制代码

上面这些方法与synchronized同步关键字配合,实现等待/通知模式。类似Object的监视器方法,Condition接口与Lock配合也可以实现等待/通知模式。但两者在使用方式和功能特性上是有所差别的。

我们来看下Object的监视方法和Condition接口的对比:

对比项Object Monitor MethodsCondition
前置条件获取对象的锁调用Lock.lock()获取锁
调用Lock.newCondition获取Condition对象
调用方式直接调用,如:object.wait()直接调用,如:condition.await()
等待队列个数一个多个
当前线程释放锁并进入等待队列支持支持
当前线程释放锁并进入等待队列,在等待状态中不响应中断不支持支持
当前线程释放锁并进入超时等待状态支持支持
当前线程释放锁并进入到等待状态到将来的某个时间不支持支持
唤醒等待队列中的一个线程支持支持
唤醒等待队列中的全部线程支持支持

Condition接口与Lock重入锁是怎么配合的那?通过lock接口(重入锁实现了这个接口)的Condition newCondition()方法可以生成一个与当前重入锁绑定的Condition实例。利用Condition对象,便可以让线程合适时间等待或在某一时刻获得通知从而继续执行。Condition是依赖Lock对象的,需要提前获取到Condition对象关联的锁。

Condition定义的基本方法

//使当前线程等待,同时释放当前锁
//跳出等待的情况:
//1.其它线程调用该Condition的sign()或者signAll()方法时,线程会重新获得锁并继续执行
//2.其它线程中断当前线程(调用interrupt()方法)
void await() throws InterruptedException

//与await()基本相似,但它不会在等待过程中响应中断
void awaitUninterruptibly()

//当前线程进入等待状态,直到被通知、中断或者超时。返回值(nanosTimeout-实际耗时)表示剩余时间。如果返回0或者负数表示已经超时
long awaitNanos(long nanosTimeout) throws InterruptedException

//当前线程进入等待状态, 直到被通知、中断或者超时。参数设置了超时时间,如果执行时间超过该时间限制还未收到通知则直接返回false
boolean await(long time, TimeUnit unit) throws InterruptedException

//前线程进入等待状态, 直到被通知、中断或某个时间。没到指定时间被通知返回true,否则,表示到了指定时间,方法返回false
boolean awaitUntil(Date deadline) throws InterruptedException

//唤醒一个在Condition等待的线程
void signal()

//唤醒所有等待在Condition上的线程
void signalAll()

复制代码

在JDK内部,重入锁和Condition对象被广泛应用,例如ArrayBlockQueue的put和take方法,有兴趣的话可以翻阅查看。

Condition的实现分析

我们先来看下newCondition的实现方法:

final ConditionObject newCondition() {
    return new ConditionObject();
}
复制代码

通过newCondition我们会获得一个ConditionObject对象,ConditionObject为同步器AQS的内部类。因为Condition操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象包含着一个等待队列,那么该队列就是Condition对象实现等待/通知功能的关键。

1.等待队列

等待队列是一个FIFO的队列,节点为Condition对象上等待的线程。重入锁维护的同步队列和Condition上的等待队列节点类型都是同步器的静态内部类AbstractQueuedSynchronizer.Node。

ConditionObject对象有下面两个属性:

/** First node of condition queue. */
//等待队列的首节点
private transient Node firstWaiter;
/** Last node of condition queue. */
//等待队列的尾节点
private transient Node lastWaiter;
复制代码

可见Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)来管理等待队列,等待队列的基本结构如下所示:

未命名文件 (3).png

如上图所示,Condition拥有首尾节点引用的单向队列,新增节点只需将原来的尾节点nextWaiter指向它,并更新尾节点即可。

在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列(lock可创建多个condiction对象),对应关系如下图所示:

未命名文件 (5).png

如图所示,同步器维护的同步队列是一个双向队列。Condition的实现是同步器的内部类,因此每个Condition实例都能够访问同步器提供的方法,相当于每个Condition都拥有所属同步器的引用。

2.等待

我们对Condition的await()方法源码来分析下:

/**
 * Implements interruptible condition wait.
 * <ol>
 * <li> If current thread is interrupted, throw InterruptedException.
 * <li> Save lock state returned by {@link #getState}.
 * <li> Invoke {@link #release} with saved state as argument,
 *      throwing IllegalMonitorStateException if it fails.
 * <li> Block until signalled or interrupted.
 * <li> Reacquire by invoking specialized version of
 *      {@link #acquire} with saved state as argument.
 * <li> If interrupted while blocked in step 4, throw InterruptedException.
 * </ol>
 */
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;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    //被中断处理
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}
复制代码

调用await()方法的前提是该线程获取了锁。我们看到添加到等待队列,调用了addConditionWaiter()方法,该方法会把当前线程构造一个新的节点加入到等待队列尾部。

**
 * Adds a new waiter to wait queue.
 * @return its new wait node
 */
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    //将当前节点包装成Node,waitStatus状态为Node.CONDITION(表明线程在该Condition等待)
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        //等待队列没有节点,则将首节点指向node
        firstWaiter = node;
    else
        //等待队列存在等待节点则将队列最后一个节点指向node
        t.nextWaiter = node;
    //更新尾节点指向node(新增加的节点插入到尾部)
    lastWaiter = node;
    return node;
}
复制代码

当前线程加入Condition等待队列过程图如下所示:

未命名文件 (6).png

加入到等待队列后,会调用fullyRelease(node)释放同步状态(释放锁)并唤醒同步队列中的后继节点,我们来看下这个方法:


/**
 * 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.waitStatus = Node.CANCELLED;
    }
}
复制代码

经过上述操作后,当前线程应该会进入等待状态,那么它是是怎么进入等待状态的那,我们看await方法中的这段代码:

while (!isOnSyncQueue(node)) {
        //当前线程进入等待状态
        LockSupport.park(this);
        //等待过程中判断是否被中断
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
复制代码

我们看到while循环体条件isOnSyncQueue(node)会判断当前节点是否在同步队列中,不在同步队列则会进入循环体执行LockSupport.park(this)阻塞进入等待。而当该线程处于等待状态的时候被中断了,由于LockSupport.park(this)不会响应中断直接返回,则会执行下面判断是否中断的条件,如果为中断则会跳出循环。

由此我们可以知道跳出该循环体要么其它线程signal()或者signbalAll()将该线程加入到同步队列并unpark唤醒该等待线程。要么响应中断。

跳出上面循环体后,后续操作就是如果中断则响应处理中断,如果被唤醒没有中断则调用AQS的acquireQueued方法自旋尝试获取锁。

3.通知

调用Condition的sign方法,会将首节点移到同步队列中并使用LockSupport唤醒节点中的线程。

signal方法如下所示:


/**
* Moves the longest-waiting thread, if one exists, from the
* wait queue for this condition to the wait queue for the
* owning lock.
*
* @throws IllegalMonitorStateException if {@link #isHeldExclusively}
*         returns {@code false}
*/
public final void signal() {
   //判断当前线程是否是获得了锁的线程,不是的话则直接抛出异常
   if (!isHeldExclusively())
       throw new IllegalMonitorStateException();
   //首节点引用节点
   Node first = firstWaiter;
   if (first != null)
       //等待队列存在节点则进行通知线程具体操作
       doSignal(first);
}
复制代码

上面代码我们可以看到具体操作在doSignal方法中,我们来看下这个方法的源码:


/**
 * Removes and transfers nodes until hit non-cancelled one or
 * null. Split out from signal in part to encourage compilers
 * to inline the case of no waiters.
 * @param first (non-null) the first node on condition queue
 */
private void doSignal(Node first) {
    do {
        //首节点指向节点为空,则表示没有节点在等待队列,则将尾节点引用置为空
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        //将首节点指向节点置为空
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}
复制代码

从上面代码我们可以看出主要是transferForSignal(first)通过首节点进行具体操作,我们继续来看下transferForSignal方法的源码:


/**
 * 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) {
    /*
     * If cannot change waitStatus, the node has been cancelled.
     */
     //将waitStatus状态从等待状态更新为0,如果更新失败则表示该节点已经被取消(waitStatus为取消状态,正如前面等待介绍的,如果await的时候被中断则node的waitStatus被置为了Node.
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添加到同步队列
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        //添加到同步队列成功则唤醒等待线程,使得线程从LockSupport.park(this)返回
        LockSupport.unpark(node.thread);
    return true;
}
复制代码

我们可以看到该方法首先会更新线程的node节点的waitStatus为0(相当于判断当前线程是否被中断,中断则更新不成功直接返回false)。然后通过enq把节点在尾部新增到同步队列并唤醒等待队列。

其中enq把节点移动到同步队列的过程如下图所示:

未命名文件 (7).png

通知还有另外一个singalAll方法:

/**
 * Moves all threads from the wait queue for this condition to
 * the wait queue for the owning lock.
 *
 * @throws IllegalMonitorStateException if {@link #isHeldExclusively}
 *         returns {@code false}
 */
public final void signalAll() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignalAll(first);
}


/**
 * Removes and transfers all nodes.
 * @param first (non-null) the first node on condition queue
 */
private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}
复制代码

singal()方法对等待队列中等待时间最长的节点(首节点)进行操作。singalAll()方法 从上面源码我们可以看到对队列中的每个节点都相当于执行了一次signal方法。将等待队列中的所有节点全部移动到同步队列,并唤醒每个节点的线程(进行锁的竞争)。

简单使用例子


public class ReentrantLockCondition implements Runnable{
    public static ReentrantLock lock = new ReentrantLock();
    public static Condition condition = lock.newCondition();

    @Override
    public void run() {
        try {
            lock.lock();
            condition.await();
            System.out.println("Thread is going on");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockCondition tl = new ReentrantLockCondition();
        Thread t1 = new Thread(tl);
        t1.start();
        Thread.sleep(2000);
        //通知线程t1继续执行
        lock.lock();
        condition.signal();
        lock.unlock();
    }
}
复制代码

线程condition.await()后进入等待状态并释放锁,主线程调用condition.signal(调用后需要释放相关的锁),通知等待的线程继续执行。

参考书籍:《Java高并发程序设计(第2版)》《Java并发编程实战》《Java并发编程的艺术》

文章分类
后端
文章标签