AQS之条件队列(独占模式)

Synchronized与AQS对比

Synchronized底层的同步队列为cxq和entryList, 等待队列为waitSet
AQS 同步队列为CLH, 条件队列为Condition
方法类比:

SynchronizedAQS
waitawait
notifysignal
nofifyAllsignalAll

方法作用类比:
wait : 把当前线程放入waitSet,调用park
await: 把当前线程放入条件队列,调用park
notify: 从waitSet中哪一个线程等待对象出来放入entrySet或者cxq。
signal: 从条件队列的队头里把一个node节点放入CLH队列末尾。

注意:notify和signal 都是不会唤醒线程的,他们只把线程放入队列里面,等待其他线程释放锁的时候被唤醒, 同样的interrupt并不会真正中断线程,相反 interrupt只是设置了一个线程中断标志位为1,然后unpark唤醒当前线程。所以要处理中断,还得业务对这个中断标识进行判断。

条件队列示意图

在这里插入图片描述
呃, CLH队列是双向的, 条件队列是单向的,切状态为conidtion,

wait 和 await 、notify和signal 都必须在同步代码块中间, 即都要在获取锁和释放锁代码中间, Synchronized只有一个waitSet, 而condition队列可以有多个

public class SemaphoreTest {
    private static ReentrantLock reentrantLock = new ReentrantLock();
    private static Object moniter = new Object();
    public static void main(String[] args) throws InterruptedException {
        /*synchronized*/
        synchronized (moniter) {
            moniter.wait();
            moniter.notify();
        }

        /*condidion*/
        Condition condition = reentrantLock.newCondition();
        reentrantLock.lock();
        condition.await();
        condition.notify();
        reentrantLock.unlock();

    }
}

await() 响应中断

首先要明确一点, 任何线程同一个时刻只会存在一个队列中, 而且执行await的方法的线程首先是要获取锁

await的具体方法逻辑三步:
第一步:进入条件队列
第二部:释放锁
第三步:阻塞
第四步:中断补偿

其中还需要特别注意的是, 第一步到第二步之间,在没有释放锁之前,因为是独占锁,所以这个期间是只有当前线程在执行,所以没有线程安全问题,所以不需要考虑多线程执行,什么cas补偿的, 但是释放锁之后 开始别的线程就可以获取锁了,那么就要以多线程思维来考虑问题

进入条件队列方法 addConditionWaiter

条件队列如上图所示, 这里还需要看看条件队列的类结构:

  /*条件队列*/
    public class ConditionObject implements Condition {

        private volatile Node firstWaiter;/*头指针*/
        private volatile Node lastWaiter;/*尾指针*/
        private static final int REINTERRUPT =  1;/*中断需要补偿*/
        private static final int THROW_IE    = -1;/*中断需要抛出中断异常*/
     }
private Node addConditionWaiter() {
            Node t = lastWaiter; /*尾节点添加*/
            // If lastWaiter is cancelled, clean out. 如果尾节点的状态是取消
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters(); /*把当前的条件队列的所有CANCLE节点都拿掉*/
                t = lastWaiter; 
            }
            Node node = new Node(Thread.currentThread(), Node.CONDITION); /*组装当前要挂起的节点*/
            if (t == null)/*还没有初始化*/
                firstWaiter = node;
            else/*已经初始化了*/
                t.nextWaiter = node;
            lastWaiter = node; /*尾指针指向*/
            return node;
        }

呃, 进入条件队列的方法整体逻辑还是比较容易想通的, 里面有个点需要进行多思考的地方就是if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); /*把当前的条件队列的所有CANCLE节点都拿掉*/ t = lastWaiter; }, 呃这个段代码的为啥这样写。。。。。, 这里需要明确一个问题条件队列和同步队列的状态有什么区别,或者说是条件队列中的节点状态有那些,同步队列中的状态又有那些

static final int CANCELLED = 1; /*同步/条件队列 线程已取消 发生了异常 */
static final int SIGNAL = -1; /同步队列 后继的线程需要被唤醒/
static final int CONDITION = -2; /*条件队列 */
static final int PROPAGATE = -3; /同步队列 共享锁/

可以看出条件队列节点的状态要么是 取消 要么是condition
哪里会使得条件队列里面的节点状态变成 CANCELLED

获取锁释放锁的时候 模板方法,tryAcquire 和tryRelease都是交给子类去实现的,所以在这个两个方法抛出异常之后,说明这个这个两个节点都需要状态变为取消

去掉已经取消的节点 unlinkCancelledWaiters

这里说实话可以出来一个疑问?就是为啥不在出异常的时候就把节点取消掉,而在是下一个节点入队的时候从头遍历处理。。。,呃,我理解如果是为了批处理感觉说法不够有理由,我觉得应该是一个比较好的习惯。

 private void unlinkCancelledWaiters() {
            Node t = firstWaiter;/*获取头节点*/
            Node trail = null; /*记录前驱非CANCLE节点的位置*/
            while (t != null) {
                Node next = t.nextWaiter; /*获取下一个节点*/
                if (t.waitStatus != Node.CONDITION) { /*条件队列里面节点的类型 要么是 CANCLE要么是CONDITION*/
                    t.nextWaiter = null; /*断开该CANCLE节点的nextWaiter指向*/
                    if (trail == null) /*说明该CANCLE节点前面没有 CONDITION节点*/
                        firstWaiter = next; /*直接把头节点指向next就行*/
                    else  /*说明前面有condition的节点 需要进行跨越 该cancle节点*/
                        trail.nextWaiter = next;
                    if (next == null)
                        lastWaiter = trail; /*到队列末尾了*/
                }
                else
                    trail = t;
                t = next;
            }
        }

完全的释放锁 fullyRelease

释放的有可能是重入的,都要把state设置为0

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; /*释放有异常*/
        }
    }

呃, 由上可见 如果释放锁release方法有异常, 节点的取消状态此时被设置上去了, 然后异常接着往外抛出去,外层的finally里面的unlock最终会释放锁,唤醒CLH同步队列的节点。

阻塞

呃 ,这里说实话有点难理解。代码也不好看。直接说一下我理解的思路吧。。。。
阻塞调用的肯定是Unsafe的底层native方法 park, 呃这方法是可以由2种方法被唤醒, 第一种就是unpark , 第二种就线程中断(线程中断底层调用的是c++的unpark操作系统级别的api)
所以说park有那些情况会被唤醒:

1、用户自己调用unpark ; 2、signal也是调用的unpark ; 3、用户中断线程
觉得那种不合理呢。。。。。, 第一种方式不合理: 如果是用户unpark自己唤醒的话, 我们需要一个循环机制继续park住, 那怎么设计这个循环机制的while条件呢?
呃, 那用户自己使用和signal的区别呢?
区别就是在于用户自己unpark, 我们是不能确定是否已经获取锁了, 应为只有获取锁的线程才能唤醒别的线程(呃,这里也不是唤醒,虽然交唤醒), 所以signal才是合法的api,他会判断当前线程是不是aqs独占锁的持有者,这样当前线程才能有能力去signal。
signal的方法逻辑 : 大概就是 需要判断条件队列节点有,那么获取第一个,然后cas修改node节点的状态从condition到0, 修改成功之后需要把node从条件队列摘除,移动到CLH队列的末尾也就是入队, 那么入队之后我们需要判断他在CLH队列中的前驱节点的状态是不是SIGNAL或者我们能把他修改成SIGNAL,因为 await之后的node的节点所代表的线程最终是应该park,所以如果前驱不是SIGNAL状态,需要唤醒这个入队的node节点代表的线程。
呃,所以这里就可以看出来 , 循环条件应该使用 判断节点是不是已经移动到了同步队列,因为用户自己unpark的线程,是不会把node节点从条件队列移动到CLH队列。

这里为什么说应该呢,诶, 想一个释放锁之后才去park的, 中间有可能有个线程已经获取到锁了, 然后signal,signal会把改节点移动到CLH同步队列,这个使用循环条件判断已经入队了,就不会去走park。

呃呃呃, 如果是用户中断呢。 用户中断也是合理的,所以我们需要对中断进行判断。。具体看代码逻辑,大概逻辑就是用户中断和别的线程signal都会去cas修改这个条件队列node节点的状态从 condition到0, 然后修改成功的就node节点进入CLH队列。

public void await() throws InterruptedException {
            /*检查是否线程已经中断了*/
            if (Thread.currentThread().isInterrupted()) {
                throw new InterruptedException();
            }
            /*包装当前线程为条件队列的node, 入队*/
            Node node = addConditonWaiter();

            /*释放锁(可能是重入的)*/
            int saveState = fullRelease(node);

            /*中断标志*/
            int interruptMode = 0;
            /*阻塞 如果node不在同步队列里面就进行阻塞*/
            while (!isOnSyncQueue(node)){
                
                LockSupport.park();
                /*判断是什么引起的被唤醒, 用户自己unpark? signal ? 用户中断?
                * 如果是用户unpark 我们得如果不进入同步队列,还是需要继续park住。
                * 如果是Signal唤醒 node已经入同步队列,所以while条件就会退出来
                * 如果是中断,呃应为设计的是响应中断,所以也要break掉。
                *   注意 : 中断 和 signal两者都需要去修改node的状态从CONDITION到0
                * */
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break; //发生中断 , interruptMode为 -1说明中断已经cas node的节点状态抢过了 signal。 1 则是没有抢过。
            }
            /*到这里说明已经进入同步队列,且被唤醒了, 得继续去抢锁*/
            if (acquireQueue(node, saveState) && interruptMode != ConditionObject.THROW_IE){
                interruptMode = REINTERRUPT; //这里补偿的中断是acquireQueue里面的中断
            }
            /*已经拿到锁了*/
            if (node.nextWaiter != null){//说明是interrupte, 如果是SIGNAL的话,nextWaiter是会被置为Null的。
                //清理清理条件队列中的CANCELLED节点
                unlindCancelledWaiters();
            }
            
            //检查中断模式
            if (interruptMode != 0){
                reportInterruptAfterWait(interruptMode);
            }
        }

判断node有没有在CLH同步队列 isOnSyncQueue

/*判断当前节点是否在同步队列上*/
    private boolean isOnSyncQueue(Node node) {
        if (node.waitState == Node.CONDITON || node.prev == null){ //条件队列node节点只有状态为CONDITION和nextWaiter指向下一`在这里插入代码片`个节点别的都为null
            return false;
        }
        if (node.next != null){
            return false;
        }
        
        return findNodeFromTail(node); //从后往前找
    }

这个要注意多思考一下:node.next为空的情况,就是cas 入队CLH队列末尾的时候的入队逻辑。所以signal唤醒入队的话。会存在node已经入队了tail也指向node了,但是node前驱的next还没有来得即指向node。

这个使用从tail往前找就可以找到:
findNodeFromTail

private boolean findNodeFromTail(Node node) {
        Node t = this.tail;
        for (;;){
            if (t == null) { //找到头了就说明没有入队
                return false;
            }
            if (t.thread == node.thread){//找到了node里面的线程都是同一个说明是用一个node
                return true;
            }
            t = t.prev;
        }
    }

判断是中断唤醒的park还是signal唤醒的park checkInterruptWhileWaiting

/*检查是不是被中断了*/
        private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?
                    (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) ://中断 但是signal是有可能一起执行的
                    0; //说明是用户自己unpark或者signal
        }

中断与signal竞争 transferAfterCancelledWait

/*中断需要干啥,需要把node节点的状态从condition修改到0*/
        private boolean transferAfterCancelledWait(Node node) {
            /*如果中断cas竞争过了 signal*/
            if (compareAndSetWaitState(node, Node.CONDITON, 0)){
                casEnqueue(node); /*入队*/
                return true;
            }
            /*没有竞争过, 那就等着signal把node从条件队列移动到同步队列*/
            while (!isOnSyncQueue(node)){
                /*礼让给 SIGNAL的线程*/
                Thread.yield();
            }
            /*此次中断失效,需要进行补偿, 没有抢过signal*/
            return false;
        }

中断唤醒与 signal唤醒的区别

呃呃呃呃, 他们都会把节点移动到CLH队列里面,但是signal会把nextWaiter置为null, 中断没有。所以如果是中断唤醒的,我们需要清理一下CANCELLED的线程,哈哈哈,这里也说明了节点状态为0也会存在条件队列中(CLH入队,node节点的状态为0)。

中断补偿或者抛出异常reportInterruptAfterWait

private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
            if (interruptMode == THROW_IE){ /*直接抛出,代表interrupte在signal之前完成*/
                throw new InterruptedException();
            }else if (interruptMode == REINTERRUPT){ /*中断位补偿,说明signal抢在interrupte前面*/
                Thread.currentThread().interrupt();
            }
        }

signal

public void signal() {
            /*当前线程不是锁的拥有者*/
            if (getOwnThread() != Thread.currentThread()) {
                throw new IllegalMonitorStateException();
            }

            Node first = firstWaiter; //获取条件队列第一个线程node节点
            if (first != null) {//有可以移动的节点
                doSignal(first); //开始尝试移动
            }

        }

doSignal

private void doSignal(Node first) {
            /*这里为啥使用while循环, 因为signal如果抢不过用户中断唤醒node的话,我们需要继续signal
            * signal代表的意思是别的线程要释放锁了,可以把条件队列的线程移动一个到CLH准备等待自己释放
            * 锁然后唤醒。 所以signal必须唤醒一个线程(除非条件队列为空了), 这样能避免造成死锁
            * */
            do {
                /*队列中只有一个节点*/
                if ((firstWaiter = first.nextWaiter) == null) {
                    lastWaiter = null;
                }
                first.nextWaiter = null;/*从条件队列中断开, 这里和中断的区别,中断不会把nextWaiter置为null*/

            } while (!transForSignal(first) && (first = firstWaiter) != null);//挪动节点,如果挪动失败(挪动失败是因为中断cas竞争修改节点状态成功了) 换下一个节点。
        }

与中断竞争 transForSignal

/*挪动节点到同步队列*/
    private boolean transForSignal(Node node) {
        if (!compareAndSetWaitState(node, Node.CONDITON, 0)) {//interrupt signal 两者竞争
            return false; //失败,挪动失败
        }
        /*状态改成功, 需要进行入队*/
        Node p = casEnqueue(node); //返回的p是node在CLH队列里面的前驱节点
        int waitState = p.waitState;
        if (waitState > 0 || !compareAndSetWaitState(p, waitState, Node.SINGAL)) {//如果前驱节点的状态是失效,或者cas不把状态修改为SIGNAL
            LockSupport.unpark(node.thread);/*前驱节点没有能力去唤醒node,需要线程就唤醒node*/
        }
        return true; //挪动成功
    }

await不响应中断 awaitUninterruptibly

/*不响应中断*/
        @Override
        public void awaitUninterruptibly() {
            //入队--条件队列
            Node node = addConditonWaiter();
            //释放锁 -- state
            int saveState = fullRelease(node);
            //阻塞
            boolean isInterrupt = false;
            while (!isOnSyncQueue(node)){
                LockSupport.park();
                if (Thread.interrupted()){
                    isInterrupt = true;
                }
            }
            //走到这里说明是有线程把node从条件队列移动到CLH,等待被唤醒
            //获取锁  这里有两种中断 acquireQueue里面的中断 和 上面while循环里面的中断
            if (acquireQueue(node, saveState) || isInterrupt){
                Thread.currentThread().interrupt();//补偿中断
            }
        }

不响应中断的意思就是 , 用户中断操作也不能唤醒在条件队列的node节点,但是中断状态还是正常补偿的,因为线程中断,park是不会生效了,就会一直循环,造成cpu浪费,所以需要消除中断,然后最后被signal唤醒的时候进行补充, 当然这里的补充包括2种, 因为有两个地方park, 一个是while里面, 一个是acquireQueue里面

超时await自动唤醒 awaitNanos

呃,这个功能的时候在于park的参数如果不为0,是可以指定阻塞时间,到了时间就自己动唤醒, 但是有一个点还是需要注意, 自己到时间唤醒之后,需要自己把自己移动到CLH队列,等待别的线程唤醒,因为是互斥锁, 你到点自己唤醒之后,AQS的锁是别的线程拥有着的, 你必须把自己送入到CLH队列,然后去尝试获取锁,这样才能互斥

public long awaitNanos(long nanosTimeout) throws InterruptedException {
            /*检查是否线程已经中断了*/
            if (Thread.currentThread().isInterrupted()) {
                throw new InterruptedException();
            }
            /*包装当前线程为条件队列的node, 入队*/
            Node node = addConditonWaiter();

            /*释放锁(可能是重入的)*/
            int saveState = fullRelease(node);

            /*中断标志*/
            int interruptMode = 0;
            /*最长的阻塞时间 绝对时间*/
            long deadLine = System.nanoTime() + nanosTimeout;
            /*阻塞 如果node不在同步队列里面就进行阻塞*/
            while (!isOnSyncQueue(node)){
                if (nanosTimeout <= 0){/*说明时间到了*/
                    /*自己把自己移动到CLH*/
                    transferAfterCancelledWait(node);
                    break;
                }
                //不用阻塞了, 时间太小了
                if (deadLine >= spinForTimeoutThreshold){
                    LockSupport.parkNanos(this, nanosTimeout);
                }
                /*判断是什么引起的被唤醒, 用户自己unpark? signal ? 用户中断?
                 * 如果是用户unpark 我们得如果不进入同步队列,还是需要继续park住。
                 * 如果是Signal唤醒 node已经入同步队列,所以while条件就会退出来
                 * 如果是中断,呃应为设计的是响应中断,所以也要break掉。
                 *   注意 : 中断 和 signal两者都需要去修改node的状态从CONDITION到0
                 * */
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break; //发生中断 , interruptMode为 -1说明中断已经cas node的节点状态抢过了 signal。 1 则是没有抢过。
                nanosTimeout = deadLine - System.nanoTime();
            }
            /*到这里说明已经进入同步队列,且被唤醒了, 得继续去抢锁*/
            if (acquireQueue(node, saveState) && interruptMode != ConditionObject.THROW_IE){
                interruptMode = REINTERRUPT; //这里补偿的中断是acquireQueue里面的中断
            }
            /*已经拿到锁了*/
            if (node.nextWaiter != null){//说明是interrupte, 如果是SIGNAL的话,nextWaiter是会被置为Null的。
                //清理清理条件队列中的CANCELLED节点
                unlindCancelledWaiters();
            }

            //检查中断模式
            if (interruptMode != 0){
                reportInterruptAfterWait(interruptMode);
            }
            return 0;
        }

总结

呃, 说实话真的挺难的。。。。。。,一定要注意那些代码是多线程执行, 那些代码逻辑是单线程执行(注:await() 响应中断)。

还有一个地方: 就是响应中断:对于一个node节点的唤醒2中情况 如果中断 cas竞争过了signal,那么需要抛出中断异常, 如果signal cas竞争过了中断,需要补充中断标志, 对于中断与signal对同一个节点的竞争,signal的while循环,中断竞争失败 Thread.yield礼让cpu给 signal去使用去移送node到CLH , 这些细节点 真的挺难的,这谁能想的到这种。。。思想。 李老爷子真的是世界级并发编程大师

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值