AbstractQueuedSynchronizer(AQS)源码解析二

这章来说一说Condition

Object中的notify、notifyAll没法唤醒一组特定的阻塞线程,只能唤醒全部中的一个或者全部(获取到锁的线程只有一个)
Condition它可以实现唤醒特定的一组线程中的一个线程,对于同一一个锁,它可以创建多个Condition,协调控制多组线程

Condition的具体实现ConditionObject

AQS中的内部类

ConditionObject重要属性

        /**
         * 	条件队列的头节点指针
         */
        private transient Node firstWaiter;
        /**
         *	 条件队列的尾节点指针
         */
        private transient Node lastWaiter;

Node与AQS源码解析一阻塞队列中的Node是一样的,这里就不解析了。

我们以ReentrantLock为例,看看ConditionObject是如何控制锁资源的获取

整体的数据结构
在这里插入图片描述
理清楚这个,再结合源码一起来理解分析,效果更佳。

ReentrantLock+Condition的典型应用

/**
 * ReentrantLock+Condition的典型使用场景
 * @author yangyangdai
 *
 */
public class ConditionDemo {
	//可重入锁的独占锁
	private Lock lock = new ReentrantLock();
	//不为满的条件 生产者阻塞 等待消费者消费数据之后唤醒
	private Condition notFull = lock.newCondition();
	//不为空的条件 消费者阻塞 等待生产者生产数据之后唤醒
	private Condition notEmpty = lock.newCondition();
	//对象数组
	private Object[] items = new Object[5];
	//生产数据的下标 消费数据的下标
	private int putIndex,takeIndex;
	//数据的数量
	private AtomicInteger count = new AtomicInteger(0);
	/**
	 * @Description: 生产数据
	 */
	public void put(Object o,int i) {
		lock.lock();
		try {
			while(count.get() == items.length) {
				notFull.await();
			}
			items[putIndex] = o;
			if(++putIndex == items.length) {
				putIndex = 0;
			}
			count.incrementAndGet();
			System.out.println("线程 " + i +  "-->生产数据 " + o);
			notEmpty.signal();
		} catch (Exception e) {
			
		}finally {
			lock.unlock();
		}
	}
	/**
	 * @Description: 消费数据
	 */
	public Object take(int i) {
		lock.lock();
		try {
			while (count.get() == 0) {
				notEmpty.await();
			}
			Object o = items[takeIndex];
			if(++takeIndex == items.length) {
				takeIndex = 0;
			}
			count.decrementAndGet();
			System.out.println("线程 " + i +  "-->消费数据 " + o);
			notFull.signal();
			return o;
		} catch (Exception e) {
			
		}finally {
			lock.unlock();
		}
		return null;
	}
}

阻塞的对象数组实现:
ReentrantLock:

保证某一时间点,只有一个线程能生产或者消费数据。

Condition:

没有数据时,会阻塞所有的消费者线程,并释放独占锁,等待生产者生产数据,然后唤醒某个阻塞的消费者线程。
数据已满时,会阻塞所有的生产者线程,并释放独占锁,等待消费者消费数据,然后唤醒某个阻塞的生产者线程。

从上面的代码中我们可以看到Condition中两个重要的方法:await()和signal(),现在我们就来AQS是如何实现的。

await()

在这里插入图片描述
添加节点到condition的条件队列中
在这里插入图片描述
释放当前节点持有的锁
在这里插入图片描述
判断当前节点是否在阻塞队列中,可能节点已经被转移到阻塞队列中了

    /**
     * 判断当前节点是否在阻塞队列中
     */
    final boolean isOnSyncQueue(Node node) {
    	//节点的waitStatus为CONDITION或者节点的前驱为null 说明不在阻塞队列中
    	//条件队列中的节点转移到阻塞队列中的时候waitStatus是为0的 
    	//waitStatus为CONDITION 肯定表示节点还在条件队列中
    	//节点的前驱为null 条件队列中的节点前驱肯定为null 因为根本没有使用到
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        //如果节点的后继不为空说明在阻塞队列中 因为条件队列中的节点后继肯定是null
        if (node.next != null) 
            return true;
        return findNodeFromTail(node);
    }

    /**
     * 直接从阻塞队列的尾部开始查找 找到就直接返回true
     * @return true if present
     */
    private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            if (t == node)
                return true;
            if (t == null)
                return false;
            t = t.prev;
        }
    }

如果节点在被唤醒,且发现自己已经在阻塞队列中,会调用acquireQueued方法,这部分是AQS源码解析一中的内容,直接去尝试获取独占锁,代码我也贴一下。

    /**
     * 返回线程被中断的状态
     * node的前驱是头节点 就再尝试获取一下锁 
     * 否则的话就挂起当前线程
     * @return true 需要将当前线程标记为中断的 false 不用标记为中断的
     */
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {//死循环
            	//获取节点的前驱
                final Node p = node.predecessor();
                //节点前驱是头节点 就再尝试获取一下锁
                if (p == head && tryAcquire(arg)) {
                	//设置当前节点为头节点
                    setHead(node);
                    p.next = null; //利于gc
                    failed = false;
                    return interrupted;
                }
                //判断node是不是需要被挂起且挂起当前线程并返回线程的中断状态
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)//如果是被中断的
                cancelAcquire(node);
        }
    }

signal()

在这里插入图片描述
看看doSignal
在这里插入图片描述
重点来了,transferForSignal,将节点从条件队列转移到阻塞队列中与我们在**await()**中说到的这断代码有关联

            //node还没有转移到阻塞队列中或者线程被中断都会终止循环
            while (!isOnSyncQueue(node)) {
                //不在阻塞队列中 挂起当前线程 当线程被唤醒或者是park的时候被中断了
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }

再来看看transferForSignal
在这里插入图片描述
enq

    /**
     * 	死循保证节点能插入到阻塞队列的队
     * 	@return 返回node的前驱节点
     */
    private Node enq(final Node node) {
        for (;;) {//死循环
            Node t = tail;
            //尾节点为空 阻塞队列中还没节点
            if (t == null) { // 必须初始化
            	//cas设置头节点 注意头节点是一个空节点
                if (compareAndSetHead(new Node()))
                	//头尾指针都指向新new空的节点
                    tail = head;
            } else {//一定会执行这步
            	//尾部插入node
                node.prev = t;
                //cas设置尾节点
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

看到这里可以知道,当转移到阻塞队列中了,节点的前驱被取消或者设置前驱的状态为SIGNAL的失败都可以直接唤醒当前线程。唤醒之后我们就会走**await()**中的,这段代码。

            //在阻塞队列中了  尝试获取独占锁并设置成之前自己的重入次数 
            //node.nextWaiter != null 说明是在signal之前中断的
            //node.nextWaiter == null 说明是在signal之后中断的
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            //节点的nextWaiter不为null 需要清空取消状态的节点
            if (node.nextWaiter != null)
                //从条件队列的头节点遍历 清除状态为取消状态的节点
                unlinkCancelledWaiters();
            //等于0说明没有发生中断 不等于0则说明发现了中断
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);

Condition整体的流程到这里我们就分析完了,关于AbstractQueuedSynchronizer完整的源码解析点击这里

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值