1.前言&目录
前言:
AQS的全称是AbstractQueuedSynchronizer,抽象队列同步器。它主要是对多线程竞争锁的过程中,以约定的规则让这些抢不到锁的线程去“乖乖排队”,这个规则就是FIFO——先进先出。
AQS全称可以拆分为抽象、队列、同步器,而且事实上AQS也是这么做的。
- 抽象之处在于AQS是一个abstract关键字修饰的类,这让我们想起了Java的模板设计模式,这种模式下的类一般是抽象的,并且其某些方法是钩子的或者是抽象的,需要留给子类去实现。
- 队列,则是AQS中存储等锁线程节点的数据结构,是使用双向链表组成的一个队列,其公平之处体现在所有线程都是按照先来后到的顺序入队,是FIFO的公平队列。
- 同步器,同步就是做一件事情需要一件件的来,不能急不能躁,在AQS体现在一把锁释放后,每次只能有一个排名最靠前的等锁节点能抢到锁,其他没抢到锁的线程需要继续在队列里等待。
AQS的同步阻塞队列模型
由于AQS源码较多,本文主要讲解AQS的核心能力,即队列的入队和出队操作。但是在AQS中又存在独占锁和共享锁,因此阻塞队列的讲解将分为上下两篇文章解读,此篇为上文——以独占锁为主进行源码解读。
目录:
2.为什么使用AQS
在程序界中,以公平与否的角度看,存在公平锁和非公平锁。在Java1.5以前,锁的实现只能通过synchronized关键字去定义,但它是非公平锁,这种情况就造成能否获取到锁似乎完全看运气了,而AQS正是为实现公平锁而生,它能让多线程以公平的方式去排队获取锁。
在Java1.5前,可以通过对方法、代码块等使用synchronized关键字确定一个并发区域——即用锁锁住某个区域、同一个时刻只能有一个线程能访问,但是它并不是一个公平锁。
因此为了弥补只有非公平锁的缺陷,在Java1.5版本推出了AQS,通过它内部维护的双向链表组成的队列可以实现公平锁。
此外,更重要的一点是AQS的锁实现比Synchronized锁更轻量化,Synchronized会导致获取不到锁的线程直接开始阻塞,而在acquireQueued源码剖析这个章节分析了,AQS是会有一个二次获取锁的过程,并不是抢不到锁就直接被阻塞。从这点考虑,AQS组成的锁机制的确更轻量化。
3.源码分析
AQS以锁的占有来说,又分为独占锁和共享锁,本文将以独占锁去讲解源码,因为独占锁的使用频率更高。
3.1 AQS类介绍
在讲解AQS的作用以前,我们需要先认识这个AQS类的基本结构以及其中的一些重要成员属性。
在以下伪代码中可以看到,AQS确实是通过abstract关键字修饰的,这也决定了不能在代码里通过new关键字去实例化一个AQS对象。但是可以通过继承它,实例化一个子类去继承AQS提供的方法(功能)。
在前言中大致介绍过,在多个线程竞争同一把锁的过程中,抢不到锁的线程都会被封装为一个节点,这个节点正是Node节点,然后以双向链表的形式实现一个等待锁的队列。正如下所示,AQS中存在head节点和tail节点,它们就是双向链表中的头节点和尾节点。对于双向链表,笔者也写了一篇介绍双向链表的文章,如果还不是很熟悉的话可以去看看。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer{
protected AbstractQueuedSynchronizer() {}
private transient volatile Node head;// 队列头节点
private transient volatile Node tail;// 队列尾节点
private volatile int state;// 同步状态
}
此外,AQS还有一个int类型的state,它会被子类用于判断当前队列是否正在同步中(即是否有线程在等待锁),主要分为0和非0两种情况,通常情况下,0表示当前线程可以“霸占”锁,非0则表示锁已经被其他线程持有了。
注意到,头节点、尾节点、state三个成员变量都是使用volatile关键字修饰的,在AQS的作用就是线程可见性,即一个线程修改了它们的值,其它线程能立刻感知并读取最新的值。
3.2 Node节点类介绍
Node节点是队列的元素,它的作用就是将等锁线程封装起来,并且通过维护前驱节点和后驱节点确定某个线程的排队顺序。当锁被释放时,排最前面的等锁节点中的线程将会得到唤醒去获取锁。
欲先善其事,必先利其器,想要掌握AQS的核心原理,我们也必须提前掌握这个Node节点的特征,下面是Node节点类的主要源码以及几个核心关键点:
- 由于Node节点是双向链表的元素,因此它拥有一个volatile修饰的prev变量和next变量(下文统称前驱节点、后驱节点)指向前后节点。
- Node节点通过volatile Thread thread去保存当前线程对象,表示将当前线程转变成一个等锁节点,然后入队到队列中去排队等待锁的释放。
- Node节点通过volatile int waitStatus变量控制当前Node节点是否可用,只有当它是小于等于0的时候表示可用,而大于0则表示当前等待锁的节点不可用了,可能需要被移除。以独占锁为例,它具体有0,-1,1三个值,0是不指定状态初始化Node的值,-1(SIGNAL)则表示当前节点在等待信号获取锁,1(CACELLED)则是当前节点被取消,不再是一个有效节点。
- 独占锁和共享锁的标记,Node SHARED是共享锁的标记,Node EXCLUSIVE是独占锁的标记,只能通过构造函数设置,不过它们仅仅起到一个标记作用。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer{
static final class Node {
static final Node SHARED = new Node(); // 共享锁的标记
static final Node EXCLUSIVE = null; // 独占锁的标记
static final int CANCELLED = 1; // 节点被取消
static final int SIGNAL = -1; // 节点在等待信号
static final int PROPAGATE = -3; // 该状态是共享锁使用的
volatile int waitStatus;// 节点等待状态,默认是-1
volatile Node prev; // 当前等锁节点的前驱节点
volatile Node next; // 当前等锁节点的后驱节点
volatile Thread thread; // 当前线程
Node nextWaiter; // 共享锁需要用到的变量
Node(){};
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode; // 共享锁会使用到这个变量判断
this.thread = thread;
}
// 获取当前节点的前驱节点,前驱节点空时会报错
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
}
}
而Node nextWaiter这个变量则是共享锁使用的,由于本文以讲解独占锁为主,不会深入讲解此变量的作用。但是请记住,它是为共享锁而生即可。
此外,Node类还提供了predecessor()方法,它的作用是返回当前Node节点的前驱节点,如果前驱节点为null时会抛出空指针异常。
经过AQS类介绍和Node节点类介绍后,我们就可以讲解AQS中的入队和出队了,这两个功能是AQS中最为的核心方法,因为入队和出队分别代表着抢不到锁的线程的排队、排队中的线程获取锁后出队。
3.3 入队
AQS的入队,更为清晰的描述为,当某个线程获取不到锁时,通过封装成Node节点加入到队列中(即通过维护AQS实例的头节点和尾节点,去让该等锁线程节点在正确位置上排队)。
如下伪代码所示,AQS#acquire(int arg)方法涵盖了锁的获取以及获取锁失败到入队的操作。涉及了三个方法:
- tryAcquire(int arg)是AQS中留给子类实现的钩子方法,它的作用就是尝试获取锁并返回是否获取到了锁,注意传入的arg参数就是用来留给子类判断当前队列同步状态的——AQS的state变量。
- addWaiter(Node.EXCLUSIVE)则是AQS中将调用tryAcquire方法失败(返回false则是当前线程获取不到锁)的线程封装为Node节点入队的操作。
- acquireQueued(addWaiter(Node.EXCLUSIVE), arg))则是AQS中将刚刚通过addWaiter方法入队的等锁节点以不间断的方式(即自旋)或者阻塞的方法去获取锁的操作。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer{
public final void acquire(int arg) {
// tryAcquire是模板方法,需要留给子类实现
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
static void selfInterrupt() { // 让当前线程尝试中断自己
Thread.currentThread().interrupt();
}
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
}
如果经过tryAcquire获取锁失败并且,acquireQueued也失败的话,则会调用当前线程的interrupt()方法尝试让自己“中断”。
3.3.1 tryAcquire源码剖析
tryAcquire方法是AQS的钩子方法,作用是获取锁并返回结果,是需要留给子类去实现。因此以ReentrantLock.FairSync重写的tryAcquire方法为例讲述获取锁的过程。
在以下伪代码可以看到,ReentrantLock.FairSync这个子类(实际上它继承的父类是内部类Sync,它又继承了AQS)重写的tryAcquire方法就是根据AQS的state变量值等于0时,由于存在多线程并发抢锁的过程,因此会通过CAS(因为state是volatile修饰的,CAS的操作可以保证正确性)的方法尝试去将AQS的state从预期的0改为传入的值(实际是1)。
一切顺利的话,会调用setExclusiveOwnerThread(current)方法将当前线程对象(引用变量)设置给AQS继承的AbstractOwnableSynchronizer的Thread exclusiveOwnerThread成员变量(独占锁才会用到)。然后返回true给调用的方法,即AQS#acquire方法表示当前线程获取到锁了。如果返回false则表示当前线程没获取到锁。
public class ReentrantLock implements Lock{
private final Sync sync;
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();// 获取AQS的state变量值
if (c == 0) {
// 用AQS的state=0?判断是否此前没有其他线程拿到锁
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// ...可重入锁的操作
return false;
}
}
}
由此可见,“锁”的这个概念和操作,其实存在于AQS的子类而非AQS本身的,即AQS并不负责获取锁的操作。
3.3.2 addWaiter源码剖析
当调用AQS#acquire方法时,会先调用AQS子类的tryAcquire方法尝试获取锁,拿到锁则返回true,跳出此方法。拿不到锁则返回false接着调用addWaiter方法。
其实addWaiter方法只做了一件事情,就是当前线程和独占锁的标记封装为一个Node节点(下文统称等锁节点)并将它添加到AQS队列的尾部——此为AQS阻塞队列的特点:FI(先进)。
如果此时尾节点不为null即当前队列不为空,先调用一次CAS操作尝试将等锁节点设置为新的尾节点。为什么这里也要通过CAS操作呢?因此在AQS类介绍的时候讲过了,头节点和尾节点和state等变量都是volatile修饰的,为了保证多个线程同时操作这些变量时能立刻感知到修改后的值。
也就是说,其实这个addWaiter方法会存在并发操作,因为它的入口之一就是AQS#acquire方法——会在AQS子类中获取锁的方法中被调用,即多线程可能同时调用某个子类的获取锁方法。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer{
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
// CAS的设置队列尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
// 通过死循环添加节点入队的方法
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 初次进入会创建一个“空”头节点,作用仅仅是作为队列头节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
// CAS的将节点添加到尾部
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
}
如果某个线程第一次CAS操作不成功或者队列本来就是空的,那么将会接着调用enq(node)方法再次将当前等锁节点入队。其细节如下:
- 如果队列为空,则新建一个没有持有线程对象(引用变量)的“空头”节点并通过CAS操作设置为队列的头节点——即队列的初始化。也就是说,等锁节点实际上是队列的从第二位开始排队的,第一的永远是这个“空头”头节点,它在这里仅仅起到了“牵头”的作用。
- 队列初始化以后,当前等锁节点同样会通过CAS操作尝试将自己设置为队列的尾节点,如果尾节点添加成功则返回并且退出enq方法——入队的操作通过CAS操作,岂不是也并非的绝对公平?其实,公平与否只是相对的一种程度,如果大多数情况下入队是按顺序,那就可以认定是公平的。
注意到,这个enq方法是一个自旋的操作(即死循环),它只有成功通过CAS方法将当前等锁节点添加到队列尾部才会停止自旋、退出循环。也就是说若多个线程同时进入到此方法想入队,也需要那么点运气~~~。
3.3.3 acquireQueued源码剖析
当一个获取不到锁的线程通过addWaiter方法成功入队到队列尾部后,该线程实际上还在正常运行着,因此仅仅入队是还不够的,在acquireQueued方法里还会给等锁线程尝试阻塞,直到有人释放锁了并通知它“不要在阻塞了,你可以继续工作了——去拿锁吧!”。
acquireQueued方法也是通过自旋的方式去进一步操作该等锁线程,主要有两个操作:
- 会判断当前等锁节点是不是排在“空头”头节点后面,即实际上的排第一位的等锁节点,并且还会尝试调用一次AQS子类的tryAcquire方法尝试二次获取锁,如果拿锁成功,则通过setHead方法将该等锁节点重新设置为新的“空”头节点(把绑定的线程变量也清空了。)——二次获取锁的过程,这一点分析就会比Synchronized更轻量化。
- 如果二次获取锁失败或者该等锁节点不是排第一位的,则通过shouldParkAfterFailedAcquire方法做一个预判——是否需要将该等锁的线程阻塞(阻塞等锁线程可能是为了不必要的自旋,减少CPU的切换)。预判过程就是将当前等锁节点的前驱节点的状态判断是否是Node.SIGNAL,是的话直接返回true表示需要阻塞。如果前驱节点已经被取消了,则向前遍历找到新的非取消的前驱节点,取消的节点就会被GC回收(因为没有引用了)。最后,将一个状态为0(节点初始化时为0)或者是-3(共享锁用到的)通过CAS操作将它设置为Node.SIGNAL并在下一次的自旋中的预判中返回true表示需要阻塞了。
public abstract class AbstractQueuedSynchronizer{
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 会再尝试获取一次锁,如果成功了直接返回
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 此节点设置 status=-1,表示当前线程需要被安全的阻塞...
return true;
if (ws > 0) {
// 等锁节点的前驱节点已经是CANCEL状态了,因此要将该不可工作的前驱节点去除掉
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// waitStatus 必须为 0 或 PROPAGATE。指示我们需要信号,
// 但暂时不要阻塞线程。调用方需要重试以确保在阻塞前无法获取。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);// 将Thread.currentThread()阻塞中。
return Thread.interrupted();
}
}
而parkAndCheckInterrupt()则会通过Thread.currentThread()获取当前的线程将其进行阻塞,也就是说。也就是说如果一个等锁线程经过tryAcquire——addWaiter——tryAcquire的调用链还无法获取到锁的话,则会阻塞在parkAndCheckInterrupt()方法中。
那么,该等锁的线程什么时候被唤醒呢?答案是在出队中调用方法unparkSuccessor被唤醒,唤醒后重新进入自旋获取锁的过程。
3.4 出队
AQS的出队,指的是实际上排在第一位的等锁线程节点(队列头部总是一个“空头”头节点,不保存线程变量,它的后继节点才是真正的等待节点),等待获取到了锁以后,将自己从队列移除出去的操作。
按照此定义,那么出队的场景在acquireQueue方法也发生过,就是在还没有预判到需要阻塞线程时,恰好此时第一的等锁节点等到了其他线程持有的锁的释放并获取后,会成为新的头节点,并置空保存的线程变量,这也是一种出队的情况。不过这种情况,概率或许比较少,并不常见,因为需要一个线程刚入队,另一个线程就释放锁。
常见的出队情况是持有锁的线程主动调用AQS#release的释放锁的操作。这个过程,也是主要做了两件事情:
- 通过调用AQS子类重写的tryRelease方法判断是否释放锁成功,成功释放锁以后才会继续调用unparkSuccessor(h)方法。
- 根据此时头节点是否为null,并且waitStatus是否不等于0,实际上如果队列中有等锁节点,在acquireQueued源码剖析中已经分析过,“空头”头节点的waitStatus会被设置为SIGNAL(-1),表示需要等待信号唤醒。而这个信号,正是来这这里——锁释放的时刻。在unparkSuccessor方法中,则会从“空头”头节点的后继节点开始,找到最前面的一个没有被取消等锁节点,将它从阻塞中唤醒转为不阻塞。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer{
public final boolean release(int arg) {
// 调用子类重写的tryRelease方法释放锁。
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
}
3.4.1 tryRelease源码剖析
tryRelease和tryAcquire一样,都是AQS中需要留给子类实现的钩子方法,它的主要作用是释放独占锁。
还是以ReentrantLock为例,它的成员变量Sync sync是AQS的子类,ReentrantLock是通过unlock()方法释放锁的,在这里会先去调用AQS的release方法,再回调到sync#tryRelease方法去释放锁。
可以看到,释放锁的过程,先判断当前的线程和绑定在AQS中获得独占锁的那个线程是否是同一个对象,如果不是则抛出异常。
接着以AQS的state变量作为判断依据,如果此时state-1=0的话,则将free变量设置为true并且将AQS中绑定的获取独占锁的线程变量置为null,最后重新设置此时AQS的state为0,表示此时可以重新去竞争锁了。如果state-1!=0,则表明此时的锁还在某个线程中持有着,将则返回false表示锁没有被成功释放。
public class ReentrantLock implements Lock {
private final Sync sync;
public void unlock() {
sync.release(1);// 调用AQS子类释放锁
}
abstract static class Sync extends AbstractQueuedSynchronizer {
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
// 还是以最终的AQS的state变量当前能否获取到锁的依据
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}
}
3.4.2 unparkSuccessor源码剖析
unparkSuccessor方法则是从“空头”头节点的后驱节点开始,找到排名最靠前的、仍然有效(即不是被取消了、waitStatus是CANCEL(1))的等锁节点,将其从阻塞状态唤醒起来去重新获取锁。
在出队方法入口处之一AQS#release中,会将此时的头节点(waitStatus是SIGNAL(-1))作为入参调用unparkSuccessor(Node node)方法,在这里会先通过CAS尝试更改此时头节点的waitStatus为0,那么这个操作有什么作用呢?其实这只是一个简单的清除“信号”的标记,在acquireQueued方法中,队列的头节点的waitStatus将会被设置为SIGNAL——表示需要等待唤醒的信号。如今已经进来了,自然要将该信号清除。
然后从“空头”头节点后继节点(后继节点们才保存了线程变量,它们才是真正的等锁节点)开始,如果此时的节点是null或者是已经被取消了,就会从队列尾部倒序遍历找到一个离“空头”头节点最近的一个有效节点(waitStatus<=0,对于独占锁来说,一般的等锁节点的等待状态都是初始化的0,只有头节点才会升级为-1。)
private void unparkSuccessor(Node node) {
// 进入unparkSuccessor方法的前提是,node<=-1,即SIGNAL或者PROPAGATE
int ws = node.waitStatus;
if (ws < 0)
// 会重新设置当前“空头”头节点的waitStatus等于0,即清除当前头节点需要
// 信号的标志
compareAndSetWaitStatus(node, ws, 0);
// 如果此时“空头”头节点的后驱节点是null或者已经被取消了,则倒序从队列
// 找到离“空头”头节点最近的有效等锁节点,然后将其唤醒
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 找到离头节点最近的、有效的头节点,将从阻塞中其唤醒
LockSupport.unpark(s.thread);
}
当最近的有效等锁节点找到以后,就会调用LockSupport.unpark(s.thread),将此时等锁节点绑定的线程从阻塞中唤醒至非阻塞——在acquireQueued源码剖析分析过,只有实际排名第一的等锁节点并且重新二次调用tryAcquire方法获得锁以后才会退出自旋操作,否则当前线程将最终会被阻塞住。
即这些没能重新获取锁得等锁线程会在这里逐个逐个的被唤醒,最终回到acquireQueued方法的自旋中重新尝试获取锁,获取到锁以后,原来的头节点会被抛弃,该节点会升级为新的“空头”头节点,其后驱节点就是下一个等待唤醒的等锁节点——即先入队的节点也是先出队的节点,AQS的FIFO就这么实现了。
4. 简单案例
经过3.源码分析分析这个章节,我们知道了AQS对于等锁节点的入队和出队操作,是公平的、FIFO先进先出的。即可通过继承AQS可以实现一个公平锁,接下来将用案例去说明这一点。
还是以 ReentrantLock的公平锁为例,在下面案例中,实现Runnable接口的TestRunnable实例的run方法,会先上锁接着输出当前线程的名称,最后释放锁。然后将其实例传入Thread实例中,调用start方法去启动线程。由于start方法中调用run方法是由本地C方法执行的,因此不能确定启动线程后,它就能立刻工作。
但是由于这里的run方法并不复杂,仅仅是输出一条语句。即如果以顺序的方法创建Thread实例并启动线程的话,很可能输出语句中的线程名称也是顺序的。果然,经过多次实验,输出语句中线程名称是顺序的概率超过了50%,因此可以认定AQS提供了一个实现公平锁的解决方案。
public class AqsLearn1 {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock(true);
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new TestRunnable(reentrantLock));
thread.setName("线程" + (i + 1));
thread.start();
}
}
static class TestRunnable implements Runnable{
private ReentrantLock reentrantLock;
public TestRunnable(ReentrantLock reentrantLock){
this.reentrantLock = reentrantLock;
}
@Override
public void run() {
try {
reentrantLock.lock();
System.out.println(Thread.currentThread().getName() + "正在工作");
}finally {
reentrantLock.unlock();
}
}
}
}
----------------输出结果----------------------
线程1正在工作
线程2正在工作
线程3正在工作
线程5正在工作
线程4正在工作
----------------输出结果----------------------
在Java1.5以前是通过synchronized关键字去定义一个并发区域的,它的锁粒度更粗、并且不是公平的,锁的获取几乎可以说是“完全靠运气”。
因此,从下面synchronized锁的案例中可以看到,经过验证,输出结果中处理线程1是基本都第一个输出,后面的四个通常是毫无顺序、规律可言,即可以认为synchronized是一个非公平锁。
public class AqsLearn1 {
public static void main(String[] args) {
Object synchronizedLock = new Object();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new TestRunnableBySynchronized(synchronizedLock));
thread.setName("线程" + (i + 1));
thread.start();
}
}
static class TestRunnableBySynchronized implements Runnable{
private Object lock;
public TestRunnableBySynchronized(Object lock){
this.lock = lock;
}
@Override
public void run() {
synchronized (lock){
System.out.println(Thread.currentThread().getName() + "正在工作");
}
}
}
}
----------------输出结果----------------------
线程1正在工作
线程4正在工作
线程3正在工作
线程5正在工作
线程2正在工作
----------------输出结果----------------------
5.总结
AQS实际上只是提供了一套通过双向链表组成的同步等待(锁)队列的解决方案,锁的获取以及释放还是需要留给AQS子类实现,AQS只不过是负责将那些获取不到锁的线程去封装为一个Node节点的保存在阻塞队列中。
在ReentrantLock.FairSync的例子中,多个线程并发调用AQS#tryAcquire方法时,只能有一个线程能获取到锁,其余的都将会入队等待锁的释放。并且是先入队的节点会先获取锁,即该队列是一个FIFO(先进先出)的,基于此特性决定了可以继承AQS可以实现公平锁。
当持有锁的线程成功释放锁以后,会进入到AQS#release方法中,在这里则会从队列中找到排名最靠前、没有被取消的等锁节点,将其从阻塞状态唤醒至非阻塞,再次去重新获取锁。
也就是说,仅以独占锁为例,AQS最重要的两个能力就是维持等锁节点的公平入队、公平出队。