并发编程(四)ReentrantLock中的Condition详解

前言

公平锁和非公平锁

公平锁和非公平锁,从语义上来说比较简单,非公平锁比较粗鲁,一上来就是直接尝试获取锁。而公平锁则会通过维护一个FIFO的阻塞队列来达到先来后到的结果。这个在上一篇博客中已经有了说明:AQS简析。具体在代码层面区别也没有太复杂,

公平锁

公平锁的tryAcquire方法。

/**
 * Fair version of tryAcquire.  Don't grant access unless
 * recursive call or no waiters or is first.
 */
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //在获取锁之前,与非公平锁相比,这里就多了一个判断,判断是否有线程在队列中等待
        if (!hasQueuedPredecessors() && 
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

非公平锁

/**
 * Performs non-fair tryLock.  tryAcquire is implemented in
 * subclasses, but both need nonfair try for trylock method.
 */
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {

        //与公平锁相比,这里就没有判断是否有线程在阻塞,而是直接去利用CAS替换state
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

上述代码就是公平锁与非公平锁获取锁的具体方法,可以看出最大的差别就是公平锁在获取锁之前会去判断是否有线程在阻塞队列中,而非公平锁并不会判断,直接就利用CAS操作去获取锁。上述代码中已经用注释标注

Condition

简单实例

实例直接上代码吧

package com.learn.condition;

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

/**
 * author:liman
 * createtime:2019/7/11
 */
public class ConditionTest {

    public static class Basket{
        Lock lock = new ReentrantLock();
        Condition producer = lock.newCondition();
        Condition consumer = lock.newCondition();
        int num = 0;
        /**
         * 生产者
         * @throws InterruptedException
         */
        public void produce() throws InterruptedException {
            lock.lock();
            System.out.println("producer get a lock......");
            try{
                while(num==1){//如果有一个商品了,生产者就阻塞
                    System.out.println("producer sleep ...");
                    producer.await();
                    System.out.println("producer awaked...");
                }

                Thread.sleep(500);
                //生产商品
                System.out.println("producer produced a object");
                num = 1;
                consumer.signal();
            }finally {
                lock.unlock();
            }
        }

        /**
         * 消费苹果
         * @throws InterruptedException
         */
        public void consumer() throws InterruptedException {
            lock.lock();
            System.out.println("consumer get a lock ...");
            try{
                while(num==0){
                    //如果没苹果,则无法消费,需要阻塞
                    System.out.println("consumer sleep ...");
                    consumer.await();
                    System.out.println("consumer awaked");
                }

                //消费商品
                Thread.sleep(500);
                System.out.println("consumer consumed an object");
                num = 0;
                producer.signal();
            }finally {
                lock.unlock();
            }
        }
    }

    /**
     * 测试basket程序
     */
    public static void testBasket(){
        final Basket basket = new Basket();
        Runnable producer = new Runnable() {
            @Override
            public void run() {
                try {
                    basket.produce();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Runnable consumer = new Runnable() {
            @Override
            public void run() {
                try {
                    basket.consumer();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        ExecutorService service = Executors.newCachedThreadPool();
        for(int i = 0;i<1;i++){
            service.submit(producer);
        }
        for(int i=0;i<1;i++){
            service.submit(consumer);
        }
        service.shutdown();
    }

    public static void main(String[] args) {
        ConditionTest.testBasket();
    }
}

上述实例比较简单,相当于维护了一个只能装的下一个物件的篮子,如果这个篮子里面有商品了,生产者就不用生产商品了,消费者可以消费商品,如果篮子中没有商品了,就需要唤醒生产者生产商品了,没错这就是典型的生产者-消费者模式。ArrayBlockingQueue其实也采用了这种设计模式,在实际开发中,我们可直接使用ArrayBlockingQueue即可。

一些概念介绍

后续就以ReentrantLock中的conditionObject来对condition机制的源码做一个分析。在此之前先说明一下,condition如果是基于ReentrantLock的,所以在进行condition中的await和signal的时候,需要先或的锁。这一点和synchronized中的obj.wait和notify一样

同时,condition上阻塞的线程会进入一个队列,这个队列不同于上一篇博客中的阻塞队列,这里为了说明清楚,就把condition上阻塞的线程队列叫条件队列,具体如下:(这个图是大牛博客中的图片,原博客地址如下:condition大牛解析)

condition-2

 signal的过程,如果简单一点理解,貌似就是将条件队列中的线程,加入到阻塞队列中,让其可重新获得锁。

相关数据结构说明:

我们会看到ConditionObject中的两个属性,这两个属性,其实就是条件队列(这个是个单向的链表)的头结点和尾节点。

并发编程(四)ReentrantLock中的Condition详解_第1张图片

 Node中的属性:(上一篇博客中针对Node 的属性其实没有贴全)

volatile int waitStatus;

volatile Node prev;//prev和next只会在阻塞队列中使用

volatile Node next;

volatile Thread thread;

Node nextWaiter;//上一篇博客没有贴出这个属性,这个属性其实就是条件队列中,Node节点的next指针(原谅这里用数据结构的概念来解释这个)

从await方法说起

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();

    //将当前线程封装成Node节点加入到条件队列中
    Node node = addConditionWaiter();

    //让当前线程完全释放锁,实质就是将state置0
    int savedState = fullyRelease(node);
    int interruptMode = 0;

    //如果不在阻塞队列中,则当前线程阻塞,
    //其实退出这个循环有两种情况:1.当前node节点进入到阻塞队列中,2.线程中断(break退出)
    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);
}

进入条件队列

addConditionWaiter方法,就是将节点加入到条件队列的方法,这个方法中的源码还比较简单,这里直接看注释应该就能理解。

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // 如果最后一个节点取消的等待状态,则清除出条件队列
    if (t != null && t.waitStatus != Node.CONDITION) {
		//遍历整个条件队列,清除指定的队列。
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
	
	//条件队列中的节点初始化的时候,waitStatus就为-2,可能这里就是上面判断如果节点状态不是-2,就清除出条件队列的原因吧
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
	
	//后面就是简单的入队操作,这个就是一般单链表的入队操作
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

上述源码中涉及到的删除节点的代码如下:

//简单的单链表删除节点操作,如果节点中的waitStatus不是-2,则直接删除
private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        //如果这个节点的状态不是-2,则将这个节点取消
        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;
    }
}

释放锁

加入条件队列之后,需要释放锁。await操作是要释放锁的,这里可以看到,是在加入条件队列中之后再去释放锁的,fullRelease就是完成这个操作的

//释放所有的锁,同时返回之前持有锁的个数
final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState();//获得之前state的值
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

进入阻塞队列

下面是最核心的代码,将线程放入阻塞队列,这里的isOnSyncQueue(node)方法就是判断线程是否在阻塞队列中的方法

int interruptMode = 0;
//如果不在阻塞队列中,则进入循环
while (!isOnSyncQueue(node)) {
    LockSupport.park(this);//线程挂起
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
        break;
}

判断是否在阻塞队列中

/**
   这里梳理一遍流程,在节点进入条件队列的时候,waitStatus=Node.CONDITION
   signal则是将节点从条件队列转入到阻塞队列,这个方法就是判断Node是否在阻塞队列中
*/
final boolean isOnSyncQueue(Node node) {

    /**
        如果ws还是等于condition,则一定在条件队列中,不在阻塞队列中,可以直接返回false
        如果prev(这个只是用于阻塞队列的变量)为null,则一定不在阻塞队列中
    */
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;

    /**
        next也是用于阻塞队列的变量,这个变量如果不为空,则一定在阻塞队列中
    */
    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.
     */
    //这个就是遍历阻塞队列,如果队列中有该Node则返回true
    return findNodeFromTail(node);
}

//遍历阻塞队列,判断是否存在指定的Node
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

signal唤醒线程

这个操作直接看下面代码中的注释即可,其实代码一行一行分析,也没想象中的那么难。

//唤醒了等待了很久的线程
public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);//调用doSignal
}

/**
    从条件队列往后遍历,找出第一个需要转移的node节点
*/
private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)//如果条件队列为空,
            lastWaiter = null;
        //first马上转移到阻塞队列,从条件队列断开,就在这个地方。
        first.nextWaiter = null;
    } while (!transferForSignal(first) && (first = firstWaiter) != null);
}

/**
	将Node从条件队列转移到阻塞队列,使得线程有资格获取锁
*/
final boolean transferForSignal(Node node) {
    /*
     * If cannot change waitStatus, the node has been cancelled.
		如果waitStatus不是CONDITION,则表示节点被取消了,就不需要转移了。
     */
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    /*
		这个和之前的入队操作一样,这里不再说明,但是需要注意的是
		enq方法返回的是节点入队之后的前驱节点。
     */
    Node p = enq(node);
    int ws = p.waitStatus;
	
	//前驱节点的ws>0或者不等于SIGNAL,
	//说明前驱节点取消了获取锁或者不需要唤醒了,这时候我们可以直接唤醒node节点
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

唤醒后的操作

还是在这段代码中,只是这个时候我们可以往下走了

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

对中断状态的处理

唤醒后的第一步是响应中断,重点是checkInterruptWhileWaiting(node)方法,这个方法具体如下,这里还是说以下THROW_IE和REINTERRUPT的状态。

  1. REINTERRUPT: 代表 await 返回的时候,需要重新设置中断状态
  2. THROW_IE: 代表 await 返回的时候,需要抛出 InterruptedException 异常
  3. 0 :说明在 await 期间,没有发生中断
//逻辑似乎不复杂,判断中断标志位,如果发生了中断,则进入transferAfterCancelledWait方法,如果没有则返回0
//transferAfterCancelledWait就是判断signal之前还是之后发生中断
private int checkInterruptWhileWaiting(Node node) {

    return Thread.interrupted() ?
    //根据transferAfterCacellWait方法的返回值,决定是返回THROW_IE还是REINTERRUPT
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}

这里先回到await方法,可以看到如果checkInterruptedWhileWaiting返回非0 ,就表示在await的时候有了中断,就会break,退出while循环。

判断是signal之前还是之后发生的中断,如下所示:

/*
只有线程发现被中断了,才会调用这个方法。(Thread.Interrutped()返回true)
*/
final boolean transferAfterCancelledWait(Node node) {
	
	/*
	替换CONDITION成功,则表示是在signal之前发生的中断。
	*/
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
	//在signal之前发生了中断,也会转移到阻塞队列中。
        enq(node);
        return true;
    }
    /*
    这里是一个保险的操作,如果node不在阻塞队列中,同时ws又不是CONDITION,说明node这个时候正在自旋
	自旋的时候,被中断,就yield,交出CPU执行的权利
     */
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

唤醒后争抢锁

/*acquireQueued方法的返回值代表线程是否被中断,但是interruptMode表示不是在进入阻塞队列之前被中断,则需要更新interruptMode*/
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
    interruptMode = REINTERRUPT;
//对于这个判断条件,之前的源码可以看到,node在进入阻塞队列之前被中断,但是并不影响node进入阻塞队列
//这个时候进入阻塞队列,是直接调用的enq,并没有在条件队列中断开连接,所以node的nextWaiter不会为空,
//但是这个时候的node是需要从条件队列中删除的
if (node.nextWaiter != null) // clean up if cancelled
    unlinkCancelledWaiters();
if (interruptMode != 0)//这一步就是处理中断,因为在唤醒后的第一步,其实只是记录了中断状态
    reportInterruptAfterWait(interruptMode);


private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    if (interruptMode == THROW_IE)//如果是在进入阻塞队列之前就被中断了
        throw new InterruptedException();
    else if (interruptMode == REINTERRUPT)//如果是在进入阻塞队列之后被中断
        selfInterrupt();
}

关于中断,这里再多总结一下,之前在看的时候,就一直不懂。就是await方法中,while循环的一段。

while (!isOnSyncQueue(node)) {
    LockSupport.park(this);
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
        break;
}

其实问题的出发点就是,如果在Node进入阻塞队列之前或者之后发生中断该如何处理,源码中依旧采用了比较优雅的方式处理中断,就是先记录中断的信号,稍后再处理,并不影响线程的后续流程。这就好比之前在介绍中断的时候,CPU告诉线程,该中断了,线程只是简单回复了一句:知道了,我稍后处理。在记录了中断状态之后(这里的状态针对是signal之前中断还是signal之后中断,做了一个区分,如果是signal之前的就记录为THROW_IE,如果是signal之后的就记录为REINTERRPUT)。记录了中断状态之后,也只是break掉当前的while循环,之后该强锁还是去强锁。并没有影响到主要的流程,只是最后再针对中断的记录作出相应的处理,while循环后面的几个if判断就是做这个事情的。

至此,关于AQS中await和signal的方法源码,已经分析的差不多了,这篇博客也是对大牛博客的一个学习总结:AbstractQueuedSynchronizer 中的 ConditionObject

总结:

针对这一篇博客的梳理还是花了相当多的经历的,这里大量篇幅还是和大牛有很多重复的地方,这篇博客也只是对大牛博客的一个学习总结。强烈建议各位去看看大牛写的文章,非常详细全面。

参考资料

AbstractQueuedSynchronizer 中的 ConditionObject

你可能感兴趣的:(多线程)