【并发编程】AQS源码分析(二)通过生产者和消费者模式理解ReentrantLock的Condition

生产者和消费者模式是并发编程中最常见的需要加锁的场景,既可以通过synchronized + 对象本身的监视器方法wait()、notify()、notifyAll()来实现等待/通知的机制,也可以通过ReentrantLock实现,而ReentrantLock方式可以结合Condition来实现等待/通知的机制。

建议:看这篇之前一定要先读懂第一篇,这样理解会容易的多
【并发编程】AQS源码分析(一) 从ReentrantLock来看AQS的基本数据结构和主要执行流程

下面生产者+消费者代码(ReentrantLock + Condition实现)

生产者+消费者

/**生产者消费者模式代码
	提前说明,这里模拟的是队列,先进先出的模式
	所以以数组为容器时,putIndex要按顺序放入,直到放满时从0开始继续放
	takeIndex也要按顺序取出,直到最大索引位置取出时,再从0开始循环取出
**/
public class ArrayBlockingQueue<E> {
	//存放元素的数组
	final Object[] items;
	//下次应该取的数组的索引,如果第一次取takeIndex = 0 的元素
	//取完后,takeIndex++;即下次取takeIndex = 1的元素
	int takeIndex;
	
	//同上,将元素放入数组下标为putIndex的位置
	int putIndex;
	//当前数组中存放的元素的总量
	int count;
	//锁
	final ReentrantLock lock;
	//条件不再为空,当数组中有元素时,此条件成立
	//数组不再为空时,可以继续从数组中取出元素
	private final Condition notEmpty;
	//条件不再满,当数组中元素不为数组最大值时,此条件成立
	//数组不再满时,可以继续放入元素
	private final Condition notFull;



	public ArrayBlockingQueue(int capacity, boolean fair) {
		if (capacity <= 0)
			throw new IllegalArgumentException();
		this.items = new Object[capacity];
		lock = new ReentrantLock(fair);
		notEmpty = lock.newCondition();
		notFull = lock.newCondition();
	}

	public ArrayBlockingQueue(int capacity) {
		this(capacity, false);
	}

	//生产者
	public boolean put(E e) throws InterruptedException {
		checkNotNull(e);
		//加锁
		lock.lock();
		try {
			//当数组已经满时
			while (count == items.length)
				//数组不再满的条件要等待,此时不能放入元素
				notFull.await();
			//代码走到这里,说明数组元素没有满,向数组中放入一个元素
			items[putIndex] = e;
			
			//放入元素后,putIndex要加一,为下一次放入准备
			if (++putIndex == items.length)
			//如果putIndex加一后发现跟数组最大值相同,说明
			//下次放入要开始循环从第一个元素的位置开始放了
				putIndex = 0;
			//放入元素后,count要自增1
			count++;
			//放入元素后,数组不再为空,通知消费者可以取元素了
			notEmpty.signalAll();

			return true;
		} finally {
			//解锁
			lock.unlock();
		}
	}

	//消费者
	public E take() throws InterruptedException {
		//加锁
		lock.lock();
		try {
			//当数组中元素为空时
			while (count == 0)
				//不为空的条件等待,此时不能从数组中取元素
				notEmpty.await();
			//当走到这里时,说明数组中已经不再为空了
			//取出一个元素
			E x = (E) items[takeIndex];
			//将相应位置为空
			items[takeIndex] = null;
			//取出一个元素是,下次取得元素时,takeIndex要自增1
			if (++takeIndex == items.length)
			//如果takeIndex自增1后 = 数组的最大长度
			//那么下次取元素时takeIndex = 0
				takeIndex = 0;
			//取出元素后count--
			count--;
			
			//取出元素后,数组不再为满,此时可以通知生产者继续放入元素了
			notFull.signalAll();
			return x;

		} finally {
			lock.unlock();
		}
	}

	private static void checkNotNull(Object v) {
		if (v == null)
			throw new NullPointerException();
	}

	/**
	 * Returns item at index i.
	 */
	@SuppressWarnings("unchecked")
	final E itemAt(int i) {
		return (E) items[i];
	}
}

我们从代码中可以看出Condition是依靠于Lock产生的。所以,在使用Condition时,必须要对对象加锁,每个Lock可以实例化出多个Condition,从源码看,一个Conditon
对应的实例化的是一个ConditionObject.

ConditionObject

//ReentrantLock类代码
final ConditionObject newCondition() {
            return new ConditionObject();
}

来看ConditionObject结构

//ConditionObject是AQS的子类
 public class ConditionObject implements Condition,
  java.io.Serializable {
        /** Node为AQS中的Node,
        条件队列的第一个等待节点 */
        private transient Node firstWaiter;
        /** 条件队列的最后一个等待节点*/
        private transient Node lastWaiter;

        /**
         * 构造方法
         */
        public ConditionObject() { }

 }

由源码可以看出,Condition其实也是由一个单向向链表构成,其中Node信息和AQS中的阻塞队列一致,二者共用Node。

把上篇文章中Node结构复制过来了如下:

	//共享模式的节点,本篇文章不涉及
	    static final Node SHARED = new Node();
        //独占模式
        static final Node EXCLUSIVE = null;

       	//waitStatus状态之一:线程 取消 ,不再获取锁
        static final int CANCELLED =  1;
        
       //waitStatus状态之一 : 表示当前线程可以被前一个节点的线程唤醒,
       //记住这唤醒是由阻塞队列中前一个节点来唤醒后一个节点的线程的
        static final int SIGNAL    = -1;
        
       //waitStatus状态之一  :条件队列的状态
        static final int CONDITION = -2;
     	
     	//waitStatus状态之一 
        static final int PROPAGATE = -3;
		
		//节点中线程等待的状态
		volatile int waitStatus;
		//前一个节点
        volatile Node prev;
		//后一个节点
        volatile Node next;

     	//当前节点锁代表的线程
        volatile Thread thread;

   		//条件队列中的等待线程
        Node nextWaiter;

那么到此为止我们可以大致总结出Condition + AQS队列的结构了:
【并发编程】AQS源码分析(二)通过生产者和消费者模式理解ReentrantLock的Condition_第1张图片
好了心中大概有了这个结构之后,再去看源码

await源码过程

/**可中断的阻塞方法
	调用此方法后,满足条件后加入到对应的Condition队列中
	等待被signal后继续执行
**/
public final void await() throws InterruptedException {
            //判断当前线程是否中断
            if (Thread.interrupted())
                throw new InterruptedException();
                
            //如果线程运行正常,将当前线程加入到条件队列中
            Node node = addConditionWaiter();
            
            //在调用await方法后,线程会释放锁,这里是释放锁的过程
            int savedState = fullyRelease(node);
            //是否中断,默认非中断
            int interruptMode = 0;
            
            /**
            	释放锁之后,这里要将当前线程挂起,挂起有两个条件
            	1、当前线程不在阻塞队列中 !isOnSyncQueue(node)
            	2、当前线程被中断了
            **/
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                //当前线程被挂起了,挂起之后重新唤醒要从这里开始执行了
                if ((interruptMode = 
                	checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
			
			/**退出循环了,这里说明已经退出挂起状态了,被其他线程唤醒了
				acquireQueued这个方法很熟悉,将Node放入阻塞队列
				开始去竞争锁了,返回true代表没有获取锁,
				返回false代表获取到了锁
			**/
            if (acquireQueued(node, savedState) && interruptMode
             != THROW_IE)
             	//这里是当没有获取到锁,并且当前线程没有被中断时,
             	//设置为中断状态
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
//将Node添加进条件队列
private Node addConditionWaiter() {
            Node t = lastWaiter;
            // 如果最后一个node是取消状态,那么把它清除出去
            if (t != null && t.waitStatus != Node.CONDITION) {
                //将条件队列中从头到尾遍历
                //将所有取消状态的Node清除出去
                unlinkCancelledWaiters();
                //将t作为最后一个条件队列的节点
                t = lastWaiter;
            }
            //将当前线程包装成一个Node,状态为 -2
            Node node = new Node(Thread.currentThread(), 
            Node.CONDITION);
            if (t == null)
            	//如果队列为空,则node作为第一个节点
                firstWaiter = node;
            else
            	//否则node作为t(原来尾节点)的下一个节点
                t.nextWaiter = node;
            //将lastwaiter指向node
            lastWaiter = node;
            return node;
        }

/**
	在向条件队列中添加节点时,会将队列中已经取消的节点移除
	将已经取消的线程移除条件队列,下面都是链表操作
**/
 private void unlinkCancelledWaiters() {
 			//从头节点开始遍历,t代表每次要判断的节点
            Node t = firstWaiter;
            //临时节点,用来记录最后一个可用节点
            Node trail = null;
            while (t != null) {
            	//记录当前节点的next
                Node next = t.nextWaiter;
               
                if (t.waitStatus != Node.CONDITION) {
                	//如果状态不是 -2,那么nextwaiter为null
                	//即将t断开连接清除出去
                    t.nextWaiter = null;
                    if (trail == null)
                    	//此时如果trail为null,t为头节点的情况
                    	//则让firstWaiter向后移动指向next
                        firstWaiter = next;
                    else
                    	//如果trail不为null,t为中间节点的情况
                    	//则直接跨过当前t指向next
                        trail.nextWaiter = next;
                    if (next == null)
                    	/**如果next为null则lastwaiter
                    	指向trail最后一个可用节点**/
                        lastWaiter = trail;
                }
                else
                	//如果符合条件,则trail指向t
                	//所以trail总是指向最后一个可用的节点
                    trail = t;
                //继续判断下一个节点是否为 -2
                t = next;
            }
        }

释放锁的过程。由于await时,必须是持有锁的状态才可以进行释放锁,由于锁重入的存在,释放锁时,有可能state = 1,也有可能state = n,所以,这里需要彻底的释放锁,无论是0还是n最后都应该state = 0;

//释放锁过程
//这里返回值saveState代表之前几次加锁的值
final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            //这里还是调用了上篇文章中说的release方法,彻底解锁
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
            	//释放锁失败后,置为取消状态
                node.waitStatus = Node.CANCELLED;
        }
    }

这里需要注意得是:
在阻塞队列中,node用到的是next,prev
而在条件队列中,node用的是nextwaiter,
在条件队列中没有进入到阻塞队列中时,next,prev应该是null

//判断node是否在阻塞队列中
//当返回false时,则线程挂起
 final boolean isOnSyncQueue(Node node) {
 		//如果状态为 -2 或者 prev为空一定不在阻塞队列中
 		//在阻塞队列中prev一定不为null
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        /**
        	如果next 不为空,那么一定在阻塞队列中
        **/
        if (node.next != null)
            return true;
        /** 下面这个方法从阻塞队列的队尾开始从后往前遍历找,
        如果找到相等的,说明在阻塞队列,否则就是不在阻塞队列

     	可以通过判断 node.prev() != null 来推断出 node 在阻塞队列吗?
     	答案是:不能。
        这个可以看上篇 AQS 的入队方法,首先设置的是 node.prev 指向 tail,
       然后是 CAS 操作将自己设置为新的 tail,可是这次的 CAS 是可能失败的。
    **/
        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;
        }
    }

代码走到这里,如果isOnSyncQueue返回false时,则线程挂起,此时就需要等待其他线程的signal了。
好了,到这里先来总结一下await方法的执行流程吧如下图:
【并发编程】AQS源码分析(二)通过生产者和消费者模式理解ReentrantLock的Condition_第2张图片
好了,await先到这里,我们先看signal方法,完了以后再看唤醒之后的流程吧

Signal源码流程

//signal线程
public final void signal() {
			//唤醒线程时,唤醒的线程必须拥有独占锁,
			//也就是你唤醒别人的条件是
			//你必须先拥有锁才可以唤醒别人
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            //唤醒的是第一个节点,等待最久的
            Node first = firstWaiter;
            if (first != null)
            	//唤醒操作
                doSignal(first);
        }
//这个方法是要将第一个条件队列中的节点转移并唤醒的操作
private void doSignal(Node first) {
        do {
          	/**
          	1、firstWaiter 指向first节点的nextwaiter
          	**/
            if ( (firstWaiter = first.nextWaiter) 
            	== null)
            	/**
					2、此时如果nextWaiter为null,
					那么说明队列为空了
        			3、所以让lastWaiter指向null
	 			**/
                  lastWaiter = null;
                  
              //走到这里,说明nextWaiter不为null,
              //那么让first节点和原来的队列断开,
              //因为他要转移走了
              first.nextWaiter = null;

		/** 
		transferForSignal(first)是要将first节点转移并唤醒
		当没有转移并唤醒成功时,让first继续指向nextwaiter(
		在do里面,已经把firstWaiter指向了nextWaiter了)
		继续循环上面的逻辑直到能转移并成功唤醒下一个节点为止
		**/
          } while (!transferForSignal(first) &&
                   (first = firstWaiter) != null);
      }
//将节点node转移进阻塞队列并在符合条件时直接唤醒
final boolean transferForSignal(Node node) {
        /*
         * 将节点的状态设置为 0
         * 这个是进入阻塞队列中的初始条件
         */
        if (!compareAndSetWaitStatus(node, 
        Node.CONDITION,  0))
            return false;

        /*
      	这个方法上篇文章说过了,
      	是通过CAS并自旋将node加入到阻塞队列的对尾
      	返回的Node p是node节点的前驱节点
         */
        Node p = enq(node);
        int ws = p.waitStatus;
        /** 
		之前说过,当前节点的唤醒要由前驱节点的状态来决定
		ws > 0 说明前驱节点已取消
		!compareAndSetWaitStatus(p, ws, Node.SIGNAL) 
		说明设置前节点唤醒状态失败
		**/
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, 
        Node.SIGNAL))
            //如果前节点不符合唤醒条件,
            //则直接唤醒当前节点中的线程
            LockSupport.unpark(node.thread);
        //否则,如果前节点符合条件的话,则不会唤醒当前节点,
        //当前节点直接进入阻塞队列
        return true;
    }

代码进行到了这里,node节点就已经从条件队列进入到了阻塞队列中去了。也就相当于重新唤醒了。

那么节点在唤醒后要沿着挂起之后的代码继续执行。

唤醒之后

唤醒之后继续执行代码,是先进行了检查中断状态

			int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
            	//线程挂起了
                LockSupport.park(this);
                /** 
                挂起之后重新唤醒之后继续执行的地方
                
                **/
                if ((interruptMode = 
                checkInterruptWhileWaiting(node)) != 0)
                    break;
            }

接着往下看

//检查waiting期间中断状态方法
/** 
	这里涉及到了两个中断状态 THROW_IE REINTERRUPT
	如果是在被唤醒前打断,则返回 THROW_IE = -1
	如果是在被唤醒之后打断的,则返回  REINTERRUPT = 1
**/
 private int checkInterruptWhileWaiting(Node node) {
 			//只有线程中断了 Thread.interrupted() 
 			//才会返回true
            return Thread.interrupted() ?
            	//只有中断了,才会调用此方法
            	//transferAfterCancelledWait
                (transferAfterCancelledWait(node) ? 
                THROW_IE : 
                REINTERRUPT) :
                0;
        }
//判断线程是signal前中断还是signal之后中断
//如果是signal之前中断返回true
//如果是signal之后中断返回false
final boolean transferAfterCancelledWait(Node node) {
		//如果CAS设置成功,说明signal之前中断
		//否则状态在进入阻塞之后已经被修改为了0了
        if (compareAndSetWaitStatus(node, Node.CONDITION,
         0)) {
        	//加入阻塞队列
            enq(node);
            return true;
        }
        /*
         *如果cas失败遍历阻塞队列判断node是否在阻塞队列中
		
			这里用while一直循环判断,个人理解
			为了等待node被signal之后进入
			阻塞队列中
         */
        while (!isOnSyncQueue(node))
        	//如果一直不在阻塞队列中,
        	//则让出cpu直到进入阻塞队列为止
            Thread.yield();
        return false;
    }

好了,检查中断的方法已经执行完了,那么继续回去看挂起之后的方法,容易晕,一定要跟着代码一步一步来

又回到了这里

			int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
            	//线程挂起了
                LockSupport.park(this);
             
                if ((interruptMode = 
                checkInterruptWhileWaiting(node)) != 0)
                //检查中断后发现线程被中断了,则直接跳出循环
                //这里很重要
                //从上面代码我们可以知道,跳出循环时,
                // ====node已经进入了阻塞队列 =====
                    break;
            }

那么继续回到await()方法里面,跳出循环后继续向下执行。上面已经说过了,即使线程被中断了,被中断的node在signal检查中断的时候也会加入到阻塞队列中去。所以在跳出循环后,开始去尝试获取锁了。

	/**
	acquireQueued在上篇文章中说过是自旋获取锁并根据
	状态判断是否要进行线程挂起 如果返回true说明线程中断,
	返回false说明获取锁成功
	savedState这里是await方法中完全释放锁之后的state的值
	
	**/
	if (acquireQueued(node, savedState) && 
	interruptMode != THROW_IE)
	//如果获取锁失败,线程中断了,
	//并且interruptMode是signal之后中断的
	//那么重新抛出中断异常
         interruptMode = REINTERRUPT;
		
		//???这里我自己一直没弄明白为什么要判空
     if (node.nextWaiter != null) 
         unlinkCancelledWaiters();
     if (interruptMode != 0)
     	//处理中断
         reportInterruptAfterWait(interruptMode);
//处理中断
  private void reportInterruptAfterWait(
  									int interruptMode)
            throws InterruptedException {
            //如果是signal之前发生的中断,直接抛出异常
            if (interruptMode == THROW_IE)
                throw new InterruptedException();
                //如果signal之后发生中断,则自我中断
            else if (interruptMode == REINTERRUPT)
                selfInterrupt();
        }

唤醒及唤醒之后的流程就到这里基本上就结束了。用一张图来总结一下。
【并发编程】AQS源码分析(二)通过生产者和消费者模式理解ReentrantLock的Condition_第3张图片
以上是关于signal和await的源码过程,有一些细节自己也不是特别清楚。
但是了解了这些代码之后,再看其他的方法的源码就会轻松很多了。

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