AQS(AbstractQueuedSynchronizer)是JAVA中众多锁以及并发工具的基础,其底层采用乐观锁,大量使用了CAS操作, 并且在冲突时,采用自旋方式重试,以实现轻量级和高效地获取锁。
提供一个框架,用于实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关的同步器(semaphore等)。 此类旨在为大多数依赖单个原子int值表示状态的同步器提供有用的基础。 子类必须定义更改此状态的受保护方法,并定义该状态对于获取或释放此对象而言意味着什么。 鉴于这些,此类中的其他方法将执行所有排队和阻塞机制。 子类可以维护其他状态字段,但是相对于同步,仅跟踪使用方法getState,setState和compareAndSetState操作的原子更新的int值。
此类支持默认独占模式和共享模式之一或两者。 当以独占方式进行获取时,其他线程尝试进行的获取将无法成功。 由多个线程获取的共享模式可能(但不一定)成功。 该类不“理解”这些差异,当共享模式获取成功时,下一个等待线程(如果存在)还必须确定它是否也可以获取。 在不同模式下等待的线程共享相同的FIFO队列。 通常,实现子类仅支持这些模式之一,但例如可以在ReadWriteLock发挥作用。 仅支持独占模式或仅支持共享模式的子类无需定义支持未使用模式的方法。
state是整个工具的核心,通常整个工具都是在设置和修改状态,很多方法的操作都依赖于当前状态是什么。由于状态是全局共享的,一般会被设置成volatile类型,为了保证其修改的可见性;
AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。队列采用的是悲观锁的思想,表示当前所等待的资源,状态或者条件短时间内可能无法满足。因此,它会将当前线程包装成某种类型的数据结构,扔到一个等待队列中,当一定条件满足后,再从等待队列中取出。
CAS操作是最轻量的并发处理,通常我们对于状态的修改都会用到CAS操作,因为状态可能被多个线程同时修改,CAS操作保证了同一个时刻,只有一个线程能修改成功,从而保证了线程安全。CAS采用的是乐观锁的思想,因此常常伴随着自旋,如果发现当前无法成功地执行CAS,则不断重试,直到成功为止。
要将此类用作同步器的基础,请使用getState,setState或compareAndSetState检查或修改同步状态,以重新定义以下方法(如适用):
tryAcquire
独占方式,arg为获取锁的次数,尝试获取资源,成功则返回True,失败则返回False。
tryRelease
独占方式,arg为释放锁的次数,尝试释放资源,成功则返回True,失败则返回False。
tryAcquireShared
共享方式,arg为获取锁的次数,尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared
共享方式,arg为释放锁的次数,尝试释放资源,如果释放后允许唤醒后续等待结点返回True,否则返回False。
isHeldExclusively
该线程是否正在独占资源。只有用到Condition才需要去实现它。
默认情况下,这些方法中的每一个都会引发UnsupportedOperationException 。 这些方法的实现必须在内部是线程安全的,并且通常应简短且不阻塞。 定义这些方法是使用此类的唯一受支持的方法。 所有其他方法都被声明为final方法,因为它们不能独立变化。
AQS中维护了一个名为state的字段,意为同步状态,是由volatile修饰的,用于展示当前临界资源的获锁情况。
下面提供了几个访问这个state字段的方法:
返回同步状态的当前值。 此操作具有volatile读取的内存语义
设置同步状态的值。 此操作具有volatile写操作的内存语义。
如果当前状态值等于期望值,则以原子方式将同步状态设置为给定的更新值。 此操作具有volatile读写的内存语义
这几个方法都是Final修饰的,说明子类中无法重写它们。我们可以通过修改State字段表示的同步状态来实现多线程的独占模式和共享模式state的值即表示了锁的状态,state为0表示锁没有被占用,state大于0表示当前已经有线程持有该锁,这里之所以说大于0而不说等于1是因为可能存在可重入的情况。你可以把state变量当做是当前持有该锁的线程数量。
exclusiveOwnerThread 属性的值即为当前持有锁的线程独占模式获取锁流程:

共享模式获取锁流程:
AQS中最基本的数据结构是Node,Node即为CLH变体队列中的节点。
AQS中CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。
在AQS中的队列是一个FIFO队列,它的head节点永远是一个虚拟结点(dummy node), 它不代表任何线程,因此head所指向的Node的thread属性永远是null。但是我们不会在构建过程中创建它们,因为如果没有争用,这将是浪费时间。 而是构造节点,并在第一次争用时设置头和尾指针。只有从次头节点往后的所有节点才代表了所有等待锁的线程。也就是说,在当前线程没有抢到锁被包装成Node扔到队列中时,即使队列是空的,它也会排在第二个,我们会在它的前面新建一个虚拟节点。
构造函数源代码
ReentrantLock 里面有三个内部类:
一个是抽象的 Sync 实现了 AbstractQueuedSynchronizer
NonfairSync 继承了 Sync
FairSync 继承了 Sync
ReentrantLock 种获取锁的方法
ReentrantLock 的非公平锁实现
compareAndSetState(0,1)
stateOffset 为AQS种维护的state属性的偏移量
setExclusiveOwnerThread(Thread.currentThread());
acquire(1); 调用的是AQS 中的acquire(int arg) 方法
tryAcquire(arg) 该方法是protected的由子类去具体实现的
我们需要看的是NonfairSync中实现的tryAcquire方法,里面又调用了nonfairTryAcquire方法,再进去看看
nonfairTryAcquire(int acquires) 方法实现
acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 方法实现,先看里addWaiter(Node.EXCLUSIVE)方法注意:Node.EXCLUSIVE 此时是空值,所以mode 就是空的,所以此时创建的Node节点中的nextWaiter是空值。
如果CLH队列的尾部节点为空值的话,执行enq(node)方法
查看 acquireQueued 方法实现
为什么前面获取锁失败了, 这里还要再次尝试获取锁呢?首先, 这里再次尝试获取锁是基于一定的条件的,即:当前节点的前驱节点就是HEAD节点,因为我们知道,head节点就是个虚拟节点,它不代表任何线程,或者代表了持有锁的线程,如果当前节点的前驱节点就是head节点,那就说明当前节点已经是排在整个等待队列最前面的了。
setHead(node); 方法
可以看出,这个方法的本质是丢弃原来的head,将head指向已经获得了锁的node。但是接着又将该node的thread属性置为null了,这某种意义上导致了这个新的head节点又成为了一个虚拟节点,它不代表任何线程。为什么要这样做呢,因为在tryAcquire调用成功后,exclusiveOwnerThread属性就已经记录了当前获取锁的线程了,此处没有必要再记录。这某种程度上就是将当前线程从等待队列里面拿出来了,是一个变相的出队操作。shouldParkAfterFailedAcquire(Node pred, Node node)方法
如果为前驱节点的waitStatus值为 Node.SIGNAL 则直接返回 true
如果为前驱节点的waitStatus值为 Node.CANCELLED (ws > 0), 则跳过那些节点, 重新寻找正常等待中的前驱节点,然后排在它后面,返回false
其他情况, 将前驱节点的状态改为 Node.SIGNAL, 返回false
acquireQueued方法中的Finally代码
非公平锁获取锁成功的流程图
非公平锁获取锁失败的流程图
尝试释放此锁。如果当前线程是此锁的持有者,则保留计数将减少。 如果保持计数现在为零,则释放锁定。 如果当前线程不是此锁的持有者,则抛出IllegalMonitorStateException。
sync.release(1) 调用的是AbstractQueuedSynchronizer中的release方法
分析tryRelease(arg)方法
tryRelease(arg)该方法调用的是ReentrantLock中
如果头节点不为空,并且waitStatus != 0,唤醒后续节点如果存在的话。这里的判断条件为什么是h != null && h.waitStatus != 0?
因为h == null的话,Head还没初始化。初始情况下,head == null,第一个节点入队,Head会被初始化一个虚拟节点。所以说,这里如果还没来得及入队,就会出现head == null 的情况。
h != null && waitStatus == 0 表明后继节点对应的线程仍在运行中,不需要唤醒
h != null && waitStatus < 0 表明后继节点可能被阻塞了,需要唤醒
为什么要从后往前找第一个非Cancelled的节点呢?看一下addWaiter方法
我们从这里可以看到,节点入队并不是原子操作,也就是说,node.prev = pred, compareAndSetTail(pred, node) 这两个地方可以看作Tail入队的原子操作,但是此时pred.next = node;还没执行,如果这个时候执行了unparkSuccessor方法,就没办法从前往后找了,所以需要从后往前找。还有一点原因,在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node。
所以,如果是从前往后找,由于极端情况下入队的非原子操作和CANCELLED节点产生过程中断开Next指针的操作,可能会导致无法遍历所有的节点。所以,唤醒对应的线程后,对应的线程就会继续往下执行。
由于篇幅较长公平锁的实现在下一篇的博客中讲述,谢谢大家的关注和支持!有问题希望大家指出,共同进步!!!