栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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 (moniter) {
            moniter.wait();
            moniter.notify();
        }

        
        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(); 
                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(); 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; 
            while (t != null) {
                Node next = t.nextWaiter; 
                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;
            }
        }

完全的释放锁 fullyRelease

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

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

呃, 由上可见 如果释放锁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 = addConditonWaiter();

            
            int saveState = fullRelease(node);

            
            int interruptMode = 0;
            
            while (!isOnSyncQueue(node)){
                
                LockSupport.park();
                
                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
        private boolean transferAfterCancelledWait(Node node) {
            
            if (compareAndSetWaitState(node, Node.CONDITON, 0)){
                casEnqueue(node); 
                return true;
            }
            
            while (!isOnSyncQueue(node)){
                
                Thread.yield();
            }
            
            return false;
        }
中断唤醒与 signal唤醒的区别

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

中断补偿或者抛出异常reportInterruptAfterWait
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
            if (interruptMode == THROW_IE){ 
                throw new InterruptedException();
            }else if (interruptMode == REINTERRUPT){ 
                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) {
            
            do {
                
                if ((firstWaiter = first.nextWaiter) == null) {
                    lastWaiter = null;
                }
                first.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);
        }
        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 = addConditonWaiter();

            
            int saveState = fullRelease(node);

            
            int interruptMode = 0;
            
            long deadLine = System.nanoTime() + nanosTimeout;
            
            while (!isOnSyncQueue(node)){
                if (nanosTimeout <= 0){
                    
                    transferAfterCancelledWait(node);
                    break;
                }
                //不用阻塞了, 时间太小了
                if (deadLine >= spinForTimeoutThreshold){
                    LockSupport.parkNanos(this, nanosTimeout);
                }
                
                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 , 这些细节点 真的挺难的,这谁能想的到这种。。。思想。 李老爷子真的是世界级并发编程大师

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1028736.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号