AQS是J.U.C阻塞式锁和相关的同步器工具的框架。AQS使用了模版方法模式,无论是J.U.C现有的同步器还是用户自定义的同步器,只需要继承AQS并实现少数几个方法(这些方法主要用于定义获取释放锁成功和失败的规则以及state变量的含义),就可以构造出一个完整的同步器。需要实现的方法如下:
而同步器的大部分公共逻辑,都已经在AQS中实现了。AQS的方法中会调用这些用户实现的方法,形成一个完整的同步器。
成员变量
AbstractQueuedSynchronizer继承了AbstractOwnableSynchronizer。后者主要定义了一个命名为exclusiveOwnerThread
的thread变量,用于存储独占模式下获取当前同步器的线程。
除此之外,AQS还有一个记录获取锁数量的state变量以及一个存储阻塞线程的sync队列,AQS存储了这个队列的头尾指针,而队列中的节点用内部类Node表示。
内部类Node:
每当同步器要阻塞或等待一个线程时,都会将该线程封装成一个Node类,放入AQS的阻塞队列或ConditionObject的等待队列中。因此Node类是构成这两种队列的节点类。阻塞队列是一个双向链表,prev,next变量指向链表的上一个节点和下一个节点。而等待队列是一个单向链表,nextWaiter指针指向下一节点。除此之外,nextWaiter还用于在阻塞队列中通过其值是否为null标记一个节点是共享节点还是互斥节点。
Node节点的waitStatus简称ws是一个状态变量。它的取值从-3至1。默认值为0,表示该结点在sync队列中等待获取锁。1为取消状态(CANCELLED),表示该结点线程取消获取锁或取消等待。-1为唤醒状态(SIGNAL),表示该结点在sync队列中需要唤醒(unpark)其后后继结点。-2为条件等待状态(CONDITION ),当节点加入到waitOject队列中时,节点的ws改为该状态。-3为传播状态(PROPAGATE),只会在共享锁释放时对头结点进行设置。
内部类ConditionObject
ConditionOject是一个条件变量,实现了Condition接口,用于等待或唤醒一个线程,其核心方法是await和signal方法。阻塞锁的newCondition方法就会创建一个该类的一个对象返回。
其内部维护一个单向的等待队列,成员变量firstWaiter,lastWaiter是等待队列的头尾指针。一个同步器可以通过newCondition方法创建多个条件变量,因此一个同步器可以有多个等待队列但只有一个同步队列。
await方法:
可中断的无限期等待,让当前线程放弃持有的锁进入等待状态,等待被其他线程中断或唤醒(signal)。只有当前线程获取了锁才能调用该方法,否则会抛出异常。
第一步:检测中断,从源码中可以看出,await方法会调用Thread.interrupted方法检测中断,如果中断抛出异常。这里印证了在中断章节学到的处于等待或限时等待的线程可以被中断,中断后会抛出异常并将中断位置0。
第二步:调用addConditionWaiter方法,将当前线程加入到等待队列中。
等待队列中的节点waitStatus只会是Condition或Cancel两种状态。可以看到,在addConditionWaiter方法中,会将新增节点设置成Condition状态加入到链表尾端。注意,如果发现之前的lastWaiter节点变成了Cancel状态,ConditionObject会先调用unlinkCancelledWaiters方法,该方法会从头开始遍历整个链表,将ws为cancel状态的节点从链表中清除。
第三步:调用fullRelease方法,该方法会调用AQS的release方法,释放线程持有的全部锁,并将该线程目前持有的锁的数目保存到saveState变量中,以便唤醒该线程后,线程会尝试获取之前数目的锁。放弃锁的具体操作是:将state变量减去saveState,将exclusiveOwnerThread置为空,并唤醒sync队列中第二个节点中的线程(sync队列的头结点为哨兵节点,里面不存储线程)。
注意:该方法中会检测当前线程是不是持有锁的线程,若不是,将抛出异常。
第四步:使用LockSupport的park方法阻塞该线程。若线程被唤醒,会循环检测本线程是否因中断或signal方法唤醒(isOnSyncQueue方法会检测当前线程的结点是否在AQS的阻塞队列中,而sigal方法的实现逻辑有一部分就是将当前结点移动到阻塞队列),如果不是这两种情况被唤醒,就继续park阻塞自己。
第五步:来到第五步,说明线程被唤醒或中断,调用acquireQueue方法,尝试在队列中获取saveState数目的锁。
至此,线程再次获取到锁后,await方法执行完毕,线程继续执行await之后的代码。
signal方法:
将Condition等待队列中的头节点(等待时间最久的结点)移动至sync阻塞队列中。同样,signal,signalAll方法必须是持有当前锁的线程才能调用,否则抛出异常。
具体操作:将Condition队列队首节点出队,将该结点的ws从Condition(-2)置为0,调用enq方法加入到Sync队列尾部,并将Sync队列中该结点的前一节点ws置为-1,用于之后唤醒该结点。
sigal与sigalAll的区别是,signal只会将等待队列中的一个节点(头结点)移除并放入到sync队列中,而signalAll方法会从头到尾遍历整个条件队列,将每一个结点都移除并放入到sync队列中。
注意:doSignal方法做循环操作是因为等待队列中的头结点ws可能是Cancel状态,这样该节点就不需要唤醒(加入到Sync队列中等待再次获取锁),直接从等待队列中移除即可,这种情况没有真正唤醒一个节点。因此需要循环操作,直到成功一次。
(总结:在并发编程中,由于会有多个线程并发进行操作,代码的执行过程非常复杂,很有可能会操作失败,这时通常需要循环操作,直到成功。因此并发中的循环语句很多时候不是用于重复执行代码,而是保证成功执行一次)
awaitNano方法:
可中断的限时等待。
与await方法基本相同,只是将第四步的park方法换成了parkNanos,若等待了设定时间后还没有被signal唤醒,会调用transferAfterCancelledWait方法,将节点加入到sync队列中尝试获取锁。
awaitUninterruptibly方法:
不可中断的无限期等待(Condition中除了该方法,其他await方法都可被中断),只有通过sigal方法才能唤醒调用该方法的线程。
如果当前线程因为中断被唤醒,记录被中断过后移除中断标记继续park阻塞自己。直到被singal唤醒后通过selfInterrupt方法重新给自己打上中断标记。
AQS核心方法
AQS的核心方法有acquire,acquireInterruptibly,tryAcquireNanos,release(独占锁的获取和释放)以及他们相应的share版本(共享锁的获取释放)。
acquire方法:
无限期阻塞线程直到成功获取锁,无法被中断,会被独占锁的lock方法调用(ReentrantLock,WriteLock)。arg表示需要获取锁的个数。
其逻辑如下:
首先调用tryAcquire方法尝试获取锁(由于不同的锁、同步器获取锁的逻辑各不相同,因此该方法由具体的Sync子类实现)如果成功,方法结束。否则将给线程加入到sync队列中(addWaiter方法)并在队列中获取锁(acquireQueued方法)。
addWaiter方法会新建一个waitStatus为0,nextWaiter为exclusive的Node节点并加入到sync队列尾端。注意,当sync队列为空也就是第一个线程入队列时,addWaiter方法会先创建一个哨兵节点(thread为空)再将该线程加入到队列的第二个节点中。之所以要这样设计是因为在sync队列中的线程基本上都会调用park方法阻塞自己,而线程无法自己调用unpark方法解除阻塞,因此在AQS中每一个阻塞了自身的线程节点都需要前一个节点唤醒自己,因此第一个加入队列的线程需要哨兵节点唤醒自己,所以排在队列的第二个位置。
加入到sync队列中线程就通过acquireQueued方法获取锁。从源码中可以看出,只有队首的线程(准确说是sync队列中第二个结点)才有资格获取锁,而其他不在队首的线程都会调用parkAndCheckInterrupt方法将自己阻塞,注意,在调用parkAndCheckInterrupt阻塞自身前会调用shouldParkAfterFailedAcquire方法检测当前线程是否可以阻塞,具体实现逻辑是检测当前结点的前置结点waitstatus状态,如果为Condition(-1),即前置结点会唤醒自己的后继结点,返回true,线程进入parkAndCheckInterrupt方法。如果为0,置为-1,返回false,线程进入下次循环。如果为Cancel(1),将该结点从Sync队列中移除,直到当前结点的前置结点不为1。
下面分析一个线程从加入到sync队列到获得锁的全过程以及为何无法被中断:
首先由于线程不在队首,第一轮循环会调用shouldParkAfterFailedAcquire方法将前置结点的ws置为-1后结束。第二轮循环会进入parkAndCheckInterrupt方法,通过LockSupport的park方法阻塞自己,此时线程停止运行。假设此时线程被中断,parkAndCheckInterrupt方法会将中断位恢复并返回true,此时interrupted变量设为true。线程进入第三次循环。在第三次循环中线程又会进入parkAndCheckInterrupt方法,由于中断位已恢复,所以线程会再次阻塞自己。这就是为什么lock方法无法被中断的底层原因。当线程节点到了队列第二个位置并且AQS锁被释放,哨兵节点会调用unPark方法唤醒线程,线程进入第四次循环,此时线程终于有资格获取锁,在调用tryAcquire获取锁成功后,哨兵节点从队列中移除,线程节点变为哨兵节点,返回值为true的interrupted变量。此时线程进入selfInterrupt方法,将中断位置1。整个acquire方法结束。
acquireInterruptibly方法:
无限期阻塞线程直到成功获取锁,中途可以被中断,会被独占锁的lockInterruptibly方法调用。
acquireInterruptibly可以理解成能够被中断的acquire方法,其实现代码基本上和acquire完全相同(tryAcquire→addWaiter→acquireQueued),唯一区别在于对中断的处理。acquire会将中断位置0,使用一个布尔变量记录中断信息然后继续阻塞,而acquireInterruptibly会直接抛出异常。因此通过该方法获取锁,可以被中断停止。
tryAcquireNanos方法:
尝试获取锁,遇到以下情况会返回:1.成功获取到锁,返回true。2.超过设定时间还没有获取到锁,返回false。3.被中断,抛出异常。会被独占锁的tryLock(Long TimeUnit)方法调用。
tryAcquireNanos可以理解成加入了限时机制的acquireInterruptibly方法,因此也是只需要在acquireInterruptibly代码上进行少量改动。与acquireInterruptibly方法主要区别是,在队列中获取锁时,每次循环都会检测是否超过设定时间,如果超时则返回false,不再尝试获取锁,另外阻塞自己时,调用的parkNanos方法,而不是park方法,这样保证了最多阻塞限定时间方法就会返回,
release方法:
释放锁,会被独占锁的unlock方法调用。arg表示释放锁的个数
AQS的release方法会先调用子类自定义的tryRelease方法释放锁,当线程将持有的独占锁全部释放完后,tryRelease会返回true,此时AQS会调用unparkSuccessor方法,唤醒sync队列中的第二个节点(逻辑意义上的头结点),如果第二个节点ws为Cancel,unparkSuccessor会跳过它寻找下一个。
acquireShared方法:
acquire方法的共享锁版本,会被共享锁调用(ReadLock.lock,CountDownLatch.await,semaphore.acquire)。
其逻辑基本与acquire方法相同,首先调用子类实现的tryAcquireShared方法尝试获取锁,如果失败,进入doAcquireShared方法,加入sync队列并在队列中获取锁。
doAcquireShared方法基本上与互斥锁的acquireQueued(addWaiter)方法完全一样,区别仅仅在于加入sync队列的是shared节点,并且线程在sync队列中成功获取到共享锁后,会调用setHeadAndPropagate方法,而互斥锁调用setHead方法。
从setHeadAndPropagate的源码从可以看到,它会先调用setHead方法将获得共享锁的节点从sync队列中移除,然后判断下一个节点是否为共享节点,如果是,继续唤醒下一个节点。下一个节点被唤醒后由于成为了头结点,也会成功获取到锁,继续唤醒下一个共享节点,直到遇到互斥节点。这样设计的代码逻辑是符合共享锁的特点的,即当一个线程获取到共享锁,其他线程也可能可以获取到共享锁。
·
acquireSharedInterruptibly,tryAcquireSharedNanos方法在acquireShared方法上做的改动与acquireInterruptibly,tryAcquireNanos在acquire方法上的改动完全一样,这里不再分析。
ReleaseShared方法与release方法逻辑基本相同,只是不同于独占锁的释放过程,共享锁的释放过程可能会因为并发原因失败(可能存在多个线程释放共享锁导致同时修改sync队列),唤醒后继结点的操作封装到了doReleaseShared方法中。在doReleaseShared方法中,AQS会不断尝试唤醒sync队列后继结点直到成功,如果唤醒失败,被将头结点ws置位PROPAGATE。
AQS中唯一没有自己实现逻辑就是tryAcquire,tryRelease和他们对应的share方法。这些方法在AQS中为空方法,完全交给子类去实现。因此,阻塞锁的tryLock方法完全由其内部的sync自己实现,不会调用AQS的源代码。