转载请联系程序员jinjunzhu公众号。java中的AQS就是AbstractQueuedSynchronizer类。AQS依赖于FIFO队列来提供一个框架来实现锁和与锁相关的同步器,例如信号量和事件。在AQS中,主要有两个功能,一个是操作状态变量,一个是实现排队和阻塞机制。注意AQS并没有实现任何同步接口,它只是提供了acquireInterruptible之类的方法,调用这些方法可以实现锁和同步器。监控模型Java使用MESA监控模型来管理类的成员变量和方法,使得对该类的成员变量和方法的操作是线程安全的。下图是MESA监控模型,其中除了定义共享变量外,还定义了条件变量和条件变量等待队列:java中的MESA模型有一点改进,就是只有一个条件变量和一个监视器内的等待队列。下图是AQS的监控模型:AQS的监控模型依赖AQS中的FIFO队列实现入口等待队列,而ConditionObject实现条件队列,可以创建多个队列。本文主要讲解入口等待队列中几种获取锁的方式。参考1[1]获得排他锁exclusive,忽略interruptspublicfinalvoidacquire(intarg){if(!tryAcquire(arg)&&acquireQueued(addWaiter(Node.EXCLUSIVE),arg))selfInterrupt();}这里tryAcquire是抽象方法AQS子类来实现,因为每个子类实现的锁是不一样的。从上述队列代码可以看出,获取锁失败后,会先执行addWaiter方法加入队列,然后执行acquireQueued方法获取锁,直到成功。addWaiter的代码逻辑如下图所示。简单来说就是将节点加入队列,加入队列后将节点参数返回给acquireQueued方法:这里有一点需要注意。如果队列为空,则会创建一个新的节点作为队列头。加入队列后获取锁acquireQueued自旋获取逻辑如下图所示:这里有几个细节:1.waitStatusCANCELED(1):当前节点取消获取锁。当等待超时或被中断(响应中断)时,会触发该状态的改变,进入该状态后节点状态不会改变。SIGNAL(-1):后续节点等待当前节点唤醒。CONDITION(-2):用在Condition中,当前线程阻塞在Condition中,如果其他线程调用了Condition的signal方法,则本节点会从等待队列转移到同步队列的尾部,等待获取同步锁。PROPAGATE(-3):共享模式,前节点唤醒后节点后,唤醒操作无条件传播。0:处于中间状态,当前节点后面的节点已经唤醒,但是当前节点线程还没有执行完毕。2、获取锁失败后挂起。如果前一个节点不是头节点,或者前一个节点是头节点但是当前节点获取锁失败,此时需要挂起当前节点。分三种情况,前节点waitStatus=-1,如下图:前节点waitStatus>0,如下图:前节点waitStatus<0但不等于-1,如下图下图:3.取消获取锁如果获取锁时抛出异常,则取消获取锁,如果当前节点是尾节点有两种情况,如下图:如果当前节点是不是尾节点,也有两种情况,如下图:4.忽略中断状态5.如果前一个节点的状态为0或者PROPAGATE,会自动被当前节点检测到更新轮换时为-1,以便稍后通知当前节点。独占+响应中断对应的方法是acquireInterruptibly(intarg)。与忽略中断(acquire方法)的区别在于响应中断。下面两个地方响应中断:在获取锁之前,会检查当前线程是否被中断。如果锁获取失败,就会入队。在队列中自旋获取锁过程中,还会检查当前线程是否被中断。如果检测到当前线程被中断,则抛出InterruptedException,当前线程退出。独占+响应中断+考虑超时对应方法tryAcquireNanos(intarg,longnanosTimeout)。该方法具有独占+响应中断+超时的功能。下面两个地方需要判断是否超时:自旋获取锁的过程中,每次获取锁失败,需要判断超时时间是否大于park失败前的超时时间。自旋的阈值时间**(spinForTimeoutThreshold=1ns)**另外,park线程的运行使用parkNanos传入阻塞时间。释放独占锁独占锁的释放分为两个步骤:释放锁和唤醒后继节点。释放锁的方法tryRelease是抽象的,由子类实现。我们看一下唤醒后继节点的逻辑。首先需要满足两个条件:头节点不等于null头节点waitStatus不等于0有两种情况(unparkSuccessor方法中):情况1,后继节点waitStatus<=0,直接唤醒后继节点,如下图:情况2:后继节点为空或者waitStatus>0,从后往前找离当前节点最近的节点唤醒,如下图:获取共享前锁,我们讲了独占锁。这一节,我们说说共享锁,有什么区别?shared,忽略中断对应方法acquireShared,代码如下:publicfinalvoidacquireShared(intarg){if(tryAcquireShared(arg)<0)doAcquireShared(arg);}tryAcquireShared这里用来获取锁的方法是tryAcquireShared,acquire是共享锁。获取共享锁和获取排他锁的区别在于,会返回一个整数值,如下所述:返回负数:获取锁失败。返回0:锁获取成功,但是后面线程获取共享锁时会失败。返回正数:获取锁成功,后续其他线程获取共享锁时也可能成功。所以需要传播唤醒操作。tryAcquireShared获取锁失败后(返回负数),加入队列后需要自旋获取,即执行doAcquireShared方法。doAcquireShared如何判断队列中的等待节点正在等待共享锁呢?nextWaiter==SHARED,这个参数值是在队列中加入新节点时由构造函数传入的。在自旋过程中,如果成功获取锁(返回正数),首先将自己设置为新的头节点,然后传播通知。如下图所示:之后,以下节点将被唤醒,唤醒操作可以传播。但是需要满足四个条件之一:tryAcquireShared返回值大于0,存在冗余锁,可以继续唤醒后继节点,老头节点waitStatus<0,应该是其他线程更新了它们的status为-3新的hade节点的waitStatus<0,只要不是tail节点,就有可能是-1,会造成不必要的wakeup,因为wakeup后无法获取到锁,所以只能继续入队,等待当前节点的后继节点是否为空。空着但等待共享锁唤醒后面节点的操作其实就是释放共享锁。对应的方法是doReleaseShared,见释放共享锁部分。share+responseinterrupt对应的方法是acquireSharedInterruptibly(intarg)。与共享忽略中断(acquireShared方法)不同的是响应中断,下面两个地方响应中断:在获取锁之前,会检查当前线程是否被中断。如果锁获取失败,就会入队。在队列中自旋获取锁过程中,还会检查当前线程是否被中断。如果检测到当前线程被中断,则抛出InterruptedException,当前线程退出。分享+响应中断+考虑超时对应方法tryAcquireSharedNanos(intarg,longnanosTimeout)。该方法具有共享+响应中断+超时的功能。下面两个地方需要判断是否超时:自旋获取锁的过程中,每次获取锁失败,需要判断超时时间是否大于park获取失败前的超时时间锁。自旋阈值时间(spinForTimeoutThreshold=1ns)另外,park线程的运行使用parkNanos传入阻塞时间。释放共享锁释放共享锁的代码如下:由子类实现。发布成功后,执行AQS中的doReleaseShared方法,即自旋操作。自旋条件是队列中至少有两个节点,这里分三种情况。情况一:当前节点的waitStatus为-1,如下图:情况二:当前节点的waitStatus为0(被其他线程更新为中间状态),如下图:情况二3:当前节点的waitStatus为-3,为什么会这样?什么?需要说明的是,在头节点唤醒后继节点之前,waitStatus已经更新为中间状态0,唤醒后继节点的动作还没有执行,被修改为-3其他线程,也就是其他线程释放了锁,执行了上面的情况2。这时候需要先把waitStatus改成0(在unparkSuccessor方法中),如下图:上面对抽象方法的解释可见一斑。如果想基于AQS实现并发锁,可以根据需要重写以下四种方法。这是AQS中没有具体实现的四个方法:tryAcquire(intarg):获取排他锁tryRelease(intarg):释放排他锁sharedlockReference2[2]AQS子类需要重写上述修改状态值的方法,定义获取或释放锁时状态值的变化。子类也可以定义自己的状态变量,但是只有更新AQS中的状态变量才会对同步产生影响。还有一个方法isHeldExclusively判断当前线程是否持有独占锁,重写后也可以被子类使用。获取/释放锁的具体实现将在下一篇文章中讲解。总结AQS使用FIFO队列实现了一个锁相关的并发模板。基于这个模板,可以实现各种锁,包括排他锁、共享锁、信号量等。在AQS中,有一个核心状态waitStatus,代表节点的状态,决定了当前节点的后续操作,比如比如是等待唤醒,还是唤醒后继节点。
