并发编程系列(十二)AQS同步器条件锁(Condition)加锁解锁源码解读

1.Condition接口

在Lock接口中有一个newCondition方法;该方法将创建一个绑定在当前Lock对象上的Condition对象,说明Condition对象和Lock对象是对应的,一个Lock对象可以创建多个Condition对象,他们是一对多的关系;Condition接口的出现是为了扩展同步代码块中的wait/notify机制

通常情况下,我们调用wait()方法,主要是因为一定的条评价没有满足,另一方面,所有调用了wait方法的线程,都会在同一个监视器的wait 中等待。这看上去很合理,但是在所有的线程等待时,会被同一个notify方法唤醒,这些唤醒的线程中也许有不满足某些条件线程被唤醒,这些线程即使拿到了监视器锁发现条件不满足还是不能够执行(还是要调用wait方法挂起),这就导致了无意义的资源浪费,

这一切的根源就在于我们调用wait方法时没有办法来指明究竟是在等待什么样的条件,因此唤醒时,也不知道唤醒哪个线程,因此最好的方式就是我们挂起的时候就指明在什么样的条件下挂起,同时在等待事件发生后,只唤醒这个事件的线程这也是Condition接口的实现

有了Condition接口,我们就可以在同一锁上创建不同的唤醒条件,从而有针对性的唤醒特定的线程而不是把等待同一把锁的线程都唤醒

2. AQS Condition条件锁实现

Condition接口是通过awat/signal机制来实现同步的,此种设计用来代替监视器锁(Monitor/Syncronized)的wait/notify机制的,因此与监视器锁的wait/notify机制对照学习有助于我们更好的理解Condition接口

同步:调用wait()方法的线程首先必须是已经进入同步代码块,即已经获取了监视器锁,与之类似,调await()方法的线程首先必须获得lock锁;

等待:调用wait方法的线程会释放已经获得的监视器锁,进入当前监视器锁,进入当前监视器锁的等待队列中;与之类似,调用监视器锁的await方法的线程会释放已经获取到的lock锁,进入到当前Condition对应的条件队列中

唤醒:调用监视器锁的notify方法会唤醒等待在该监视器器上的线程,这些线程将开始参与监视器锁的竞争,并在获得锁后,从wait方法处恢复执行,与之类似调用Condition的signal方法会唤醒对应的条件队列中的线程这些线程开始参与锁竞争, 并且在获得锁后,从await方法处开始恢复执行

3.同步队列与条件队列

同步阻塞队列(同步队列)是一个双向链表,我们使用prev,next属性来串联节点(Node),在node节点中还有一个nextWaiter属性,

3.1 条件队列

每创建一个Condition对象就会对应一个条件队列,每当调用了Condition对象的await方法的线程都被被包装成Node放入一个条件队列

每一个Condition对象对应一个条件队列,每一个队列都是独立的互不影响的,条件队列是一个单向链表,在该链表中我们使用next属性来串联链表一样,在条件队列中,也不会用到prev / next属性,他们的属性都是null,也就是说在条件队列中,Node节点真正用到的属性只有三个:

thread:代表当前正在等待某个条件的线程

waitStatus:条件等待状态

nextWaiter:指向条件队列中的下一个节点

3.2 同步队列与条件队列的联系

在一般情况下,同步阻塞队列和条件队列是相互独立的,彼此之间并没有任何关系,但是,当我们调用某个条件队列的signal方法后,会将某个或所有等待在这个条件队列中的线程唤醒,被唤醒的线程和普通线程一样需要去争锁,如果没有抢到,则同样被加到等待锁的同步队列中去,此时节点就从条件队列被转移到同步队列中;

 

在这里需要注意的是,Node节点是被一个一个转译过去的,哪怕我们调用的signalAll方法,node节点也是一个一个转移过去的而不是将整个条件队列接在同步队列末尾;同时需要注意的我们在同步队列中只有使用prev,next来串联表而不是使用nextWaiter我们在条件队列中只使用nextWaiter来串联链表,而不使用prev,next;在这就是是使用了同样的Node数据结构而完全不同的链表,因此将节点从条件队列转移到同步队列中时,我们需要断开原来的链接(nextWaiter)建立新的链接(prev / next)在某种程度上也是需要将节点一个一个转移过去的原因之一。

3.3入队和出队时的锁状态 

同步队列是等待锁的队列,当一个线程被包装成node加到改队列中时,必然是没有获取到锁;当处于改队列中的节点获取到锁,它将从改队列中移除(事实上移除操作是将获取到的锁节点设为新的dummy heed并将thread属性设置为null)

条件队列是等待锁的队列,当一个线程被包装成Node加入到改队列中时,必然没有获取到锁,当处于队列中节点获取到锁,他将从该队列中移除;条件队列是等待在特定条件下的队列,因为调用await方法时,必然已经获得到lock锁,所以进入条件队列前必然已经获取了锁,在别包装成node扔进条件队列后,线程释放锁,然后挂起,当处于该队列中线程被signal方法唤醒后,由于队列中节点在之前挂起的时候已经释放了锁,所以必须先再去竞争锁,因此,改节点会被添加到队列中,条件队列咋出队时,线程并不持有锁

条件队列:入队钱已经持有锁,在队列中释放锁,离开队列时没有锁 转移到同步队列

同步队列:入队前没有锁,在队列中争锁,离开队列时候获取到锁

4. Conditionobject 源码分析

AQS对Condition这个接口实现主要通过ConditionObject,上面已经说过这个,他的核心就是实现一个条件队列,每一个在某个condition上等待的线程都会被封装成node对象放入条件队列

4.1await()方法解析

        public final void await() throws InterruptedException {
            //如果当前线程在调用await方法前被中断,则直接抛出interruptedException
            if (Thread.interrupted())
                throw new InterruptedException();
            //将当前线程封装成node添加到条件队列
            Node node = addConditionWaiter();       
            //释放当前线程所占用的锁,保持当前锁的状态
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            //如果当前队列不在同步队列中,说明刚刚被wait,还没有其他线程调用signal方法,则直接将当前线程阻塞
            while (!isOnSyncQueue(node)) {
                //线程在这里被阻塞,停止运行
                LockSupport.park(this);
                //能执行到这里说明1:调用signal方法唤醒,2:线程被中断
                //所以检查下线程被唤醒的原因,如果是因为中断被唤醒,则跳出while循环
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //
            //上面的代码代表条件队列中的线程先阻塞,在被唤醒(signal唤醒 / 被中断唤醒)
            //下面代码我么之后在解释
            //
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

我们先来看看await方法中调用的方法-addConditionWater()方法

await方法首先调用addConditionWater方法将当前线程封装成node放入条件队列

        /**
        *将当前线程封装为node放入条件队列
         */
        private Node addConditionWaiter() {
           //获取队尾结点
            Node t = lastWaiter;
            //如果队尾结点被cancal了,则先遍历整个链表,清除所有被cancal的结点
            if (t != null && t.waitStatus != Node.CONDITION) {
                //清除那些已经取消等待的线程
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            //将当前线程包装成node放入条件队列
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            //如果队尾结点为空,这时队列无节点,则设置node为头结点
            if (t == null)
                firstWaiter = node;
            else
                //如果队尾结点非空,设置队尾结点的nextWaiter执向node结点
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

在这里存不存在两个线程同时入队的情况呢?答案是肯的不存在,因为我们前面已经说过,能够调用await方法的线程必然是已经获得锁的,而获得锁线程只有一个,所以这里不存在并发的情况,因此不需要CAS操作;

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

发布了55 篇原创文章 · 获赞 3 · 访问量 5249

猜你喜欢

转载自blog.csdn.net/qq_38130094/article/details/103679146