您的位置:首页 > 产品设计 > UI/UE

AQS(AbstractQueuedSynchronizer)源代码分析(一)——实现逻辑概览

2018-02-11 09:04 579 查看
      在学习java的锁机制时,一定会遇到AbstractQueuedSynchronizer类的实现。它几乎是java锁机制的核心,几乎所有的锁都是基于AbstractQueuedSynchronizer类实现的。所以很有必要看一下AbstractQueuedSynchronizer的源代码实现,但是直接看AQS源代码,很容易就绕晕了。主要是因为我们没能掌握AQS的运行机制,直接阅读细节实现,导致很难看得懂。所以本文重点说一下AQS的整体实现思路,方便后面进行源代码的分析。
       AbstractQueuedSynchronizer示例说明(高铁检票进站):
       AbstractQueuedSynchronizer的实现过程与高铁检票很类似,高铁检票有四个特征:
       1、检票口的数量是有限的(这里我们暂时只讨论一个检票口)
       2、等待检票的人数一定是大于检票口的数量的,所以会有排队(不排队的情况也有,只是比较少;不排队也是排队的一种特殊情况)
       3、如无意外,每次都是排在队首的人(即第一个人)能够检票进入;
       4、如果出现意外,比如:有老人或者其他原因,可能会有插队的情况(这里不考虑特殊通道)。
示例图如下:



检票场景描述:
      1、 当第一个人在检票的时候,后面所有的人都会等着(WAITING)。当第一个人检票完成后,我们会接收到一个信号“我可以检票了”(别人检票完成这个动作默认就给我们发出了一个信号)。然后我们就会去检票,如果检票成功,则进站;这个时候队列就少了一个人。
      2、如果等到我们检票的时候,突然有个人跑过来,说自己腿脚不方便或者其他XX(总之就是一顿说明,其实就是竞争检票口);很不幸,我们被他说服了,让他先检票;这个时候,我们又会处于等待状态(WAITING)。等到刚才那个人检票完成后,我们又接收到信号(他检票完了,我可以检票了)。此时,我们上前去进行检票。当然,这个时候还是有可能出现第二次竞争的,万一又跑来一个插队的,又是一顿说明。这次他没有说服你(我们竞争成功),这个时候想插队的那个人就会排到队队尾。我们检票进站。如下图:



上图演示了AQS的一个基本的处理过程,AQS就是基于这种方式来实现的。
       AbstractQueuedSynchronizer实现思路说明:
       AbstractQueuedSynchronizer维护了一个node队列和一个state状态,然后通过aquired(获取)和release(释放)两类方法操作这个队列和状态(留意一下,这里说的是“两类”方法,也就是说aquired和release方法有多个重载版本,后文再做讨论)。node队列表示每一个等待的线程,一个线程一个node节点(相当于等待检票的人),state表示当前锁的状态(相当于检票口的状态)。
       AbstractQueuedSynchronizer维护的node队列并不是我通常意义上说的queue队列,一种类似集合或者数组的东西。在AbstractQueuedSynchronizer中,node队列是通过一个一个对象组成的,而且这些队列之间相互的引用,也就形成了一种队列。看下图:



在每一个NODE中,都会持有两个引用(prev和next),用来分别指向自己的上一个节点和下一个节点,后文税说明具体的代码实现,这里只说实现思路。
       AQS操作的第一阶段:获取。如图:
       


上图简要的说明了AQS的获取过程,其实就是尝试修改sate状态的过程,如果修改成功,就会执行被锁定的内容,也就是我们需要同步的那部分代码。如果修改失败,则继续尝试修改(这种说法并不严谨,这里先这样理解,在第二阶段:释放,会具体说明这里的实现方式)。
      这里需要注意的是:
      1、每个进入的线程都会进行一次“尝试修改sate”的动作,相当于跟第一个检票人竞争检票口。如果竞争成功,则直接执行;如果失败,则排到node队列的末尾。
      2、上图的循环图标只是表示会循环尝试修改sate,但是并不会循环的去追加Node节点。每一个线程进行第一次尝试修改sate时,如果修改失败,AQS就会用当前线程创建一个node节点,追加在队列末尾。也就是说一个线程只会有一个节点。当进行第二次修改sate时,是不会再新增node了。
      AQS操作的第一阶段:释放。如图:



上图展示的是AQS的一个完整的获取和释放的过程。这里重点看release部分:
首先,在执行完被锁定的内容之后,就需要将sate还原为原始的状态,让其他线程可以执行。所以这里会尝试还原sate状态,如果还原成功,则唤醒WAITING中的线程。
其次,如果还原失败,这里还原失败并不会进行循环尝试。这是因为,在release时,还原失败并不是真正的失败了,而是因为sate被这个线程修改了多次,所以一次release并不能够将sate还原为“初始状态”,所以还需要等待下一次的release。
最后,还需要在说明一下“循环尝试修改sate”,当想成第一次尝试修改sate失败后,AQS就会把它挂起(相当于调用了Object.wait()方法),此时线程是处于WAITING睡眠状态的,需要其他线程或者操作将其唤醒。因此在其他线程执行完被锁定的内容后,会还原sate,同时也会唤醒处于WAITING中的线程。伪代码如下(AQS的代码并不是这样写的,这里只是方便理解)://伪代码,获取
synchronized (user) {
while(!tryAquired()){ //线程再次被唤醒时,会再次校验sate。这个校验的过程就是尝试修改sate的过程
currentThread.wait();
}
doSomeThing();//执行被锁定的内容
}

tryAquired(){
if(sate == "sate的初始值"){
//修改sate,可以是sate+1,也可以是sate-1,或者任何一种变化,这里是由AQS的子类决定的;
sate++;
return true;
}else{
return false
}
}
//伪代码:释放
doSomeThing();//执行被锁定的内容完成之后
if(tryRelease()){//释放,还原sate,如果还原成功,则唤起线程
Object.notify(); //唤起下一个线程,notify方法是随机唤起,这里只是说明唤起操作,AQS并不是使用notify方法唤醒的
};
      上面展示的获取和释放两个阶段是AQS核心的实现部分,主要在于可以理解AQS整个的运行机制时什么样子,以便于在下一节进行源代码分析时,不会被绕晕了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息