文章目录
一、简介
AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,是Java中众多锁以及并发工具的基础,其底层采用乐观锁,大量使用了CAS操作, 并且在冲突时,采用自旋方式重试,以实现轻量级和高效地获取锁。
AQS虽然被定义为抽象类,但事实上它并不包含任何抽象方法。这是因为AQS是被设计来支持多种用途的,如果定义抽象方法,则子类在继承时必须要覆写所有的抽象方法,这显然是不合理的。所以AQS将一些需要子类覆写的方法都设计成protect
方法,将其默认实现为抛出UnsupportedOperationException
异常。如果子类使用到这些方法,但是没有覆写,则会抛出异常;如果子类没有使用到这些方法,则不需要做任何操作。
AQS 定义两种资源共享方式:独占式,共享式。这样方便使用者实现不同类型的同步组件,独占式如ReentrantLock,共享式如Semaphore,CountDownLatch,组合式的如ReentrantReadWriteLock。
自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体等待线程队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
isHeldExclusively()
:该线程是否正在独占资源。只有用到condition才需要去实现它。tryAcquire(int)
:独占方式。尝试获取资源,成功则返回true,失败则返回false。tryRelease(int)
:独占方式。尝试释放资源,成功则返回true,失败则返回false。tryAcquireShared(int)
:共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。tryReleaseShared(int)
:共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
二、Java并发工具类的三板斧
在开始看AQS源码之前,我们先来了解以下java并发工具的设计套路,我把它总结成三板斧:
状态,队列,CAS
每当我们学习一个java并发编程工具的时候,我们首先要抓住这三点。
- 状态:一般是一个
state
属性,它基本是整个工具的核心,通常整个工具都是在设置和修改状态,很多方法的操作都依赖于当前状态是什么。由于状态是全局共享的,一般会被设置成volatile类型,以保证其修改的可见性; - 队列:队列通常是一个等待的集合,大多数以链表的形式实现。队列采用的是悲观锁的思想,表示当前所等待的资源,状态或者条件短时间内可能无法满足。因此,它会将当前线程包装成某种类型的数据结构,扔到一个等待队列中,当一定条件满足后,再从等待队列中取出。
- CAS: CAS操作是最轻量的并发处理,通常我们对于状态的修改都会用到CAS操作,因为状态可能被多个线程同时修改,CAS操作保证了同一个时刻,只有一个线程能修改成功,从而保证了线程安全,CAS操作基本是由Unsafe工具类的
compareAndSwapXXX
来实现的;CAS采用的是乐观锁的思想,因此常常伴随着自旋,如果发现当前无法成功地执行CAS,则不断重试,直到成功为止,自旋的的表现形式通常是一个死循环for(;;)
。
三、AQS核心实现
状态
首先是找状态。在AQS中,状态是由state属性来表示的,不出所料,它是volatile类型的:
private volatile int state;
该属性的值即表示了锁的状态,state为0表示锁没有被占用,state大于0表示当前已经有线程持有该锁,这里之所以说大于0而不说等于1是因为可能存在可重入的情况。你可以把state变量当做是当前持有该锁的线程数量。
通过state变量是否为0,我们可以分辨当前锁是否被占用,但光知道锁是不是被占用是不够的,我们并不知道占用锁的线程是哪一个。在监视器锁中,我们用ObjectMonitor对象的_owner属性记录了当前拥有监视器锁的线程,而在AQS中,我们将通过exclusiveOwnerThread属性:
private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer
exclusiveOwnerThread
属性的值即为当前持有锁的线程。
队列
接着我们来看队列,AQS中的队列是一个CLH队列,队列的实现是一个双向链表,被称为sync queue
,它表示所有等待锁的线程的集合,有点类似于synchronized原理中的wait set
。
在并发编程中使用队列通常是将当前线程包装成某种类型的数据结构扔到等待队列中,我们先来看看队列中的每一个节点是什么结构:
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
这个结构看起来很复杂,其实属性只有4类:
// 节点所代表的线程
volatile Thread thread;
// 双向链表,每个节点需要保存自己的前驱节点和后继节点的引用
volatile Node prev;
volatile Node next;
// 线程所处的等待锁的状态,初始化时,该值为0,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、 //<0来判断结点的状态是否正常。
volatile int waitStatus;
//表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
static final int CANCELLED = 1;
//表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
static final int SIGNAL = -1;
//表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列 //中,等待获取同步锁。
static final int CONDITION = -2;
//共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
static final int PROPAGATE = -3;
// 该属性用于条件队列或者共享锁
Node nextWaiter;
注意,在这个Node类中也有一个状态变量waitStatus
,它表示了当前Node所代表的线程的等待锁的状态。这里还有一个nextWaiter
属性,它在独占锁模式下永远为null
,仅仅起到一个标记作用,没有实际意义。
说完队列中的节点,我们接着说回这个sync queue
,AQS是怎么使用这个队列的呢,既然是双向链表,操纵它自然只需要一个头结点和一个尾节点:
// 头结点,不代表任何线程,是一个哑结点
private transient volatile Node head;
// 尾节点,每一个请求锁的线程会加到队尾
private transient volatile Node tail;
到这里,我们就了解到了这个sync queue
的全貌:
CAS操作
CAS操作大多数是用来改变状态的,在AQS中也不例外。我们一般在静态代码块中初始化需要CAS操作的属性的偏移量:
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;
static {
try {
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
headOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
tailOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
waitStatusOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("waitStatus"));
nextOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("next"));
} catch (Exception ex) { throw new Error(ex); }
}
从这个静态代码块中我们也可以看出,CAS操作主要针对5个属性,包括AQS的3个属性state
,head
和tail
, 以及Node对象的两个属性waitStatus
,next
。说明这5个属性基本是会被多个线程同时访问的。
定义完属性的偏移量之后,接下来就是CAS操作本身了:
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
private static final boolean compareAndSetWaitStatus(Node node, int expect,int update) {
return unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update);
}
private static final boolean compareAndSetNext(Node node, Node expect, Node update) {
return unsafe.compareAndSwapObject(node, nextOffset, expect, update);
}
如前面所说,最终CAS操作调用的还是Unsafe类的compareAndSetXXX方法。
四、独占式
1.acquire()
acquire 定义在AQS类中,描述了获取锁的流程
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
a.首先,调用使用者重写的tryAcquire方法,若返回true,意味着获取同步状态成功,后面的逻辑不再执行;若返回false,也就是获取同步状态失败,进入b步骤;
b.此时,获取同步状态失败,构造独占式同步结点,通过addWatiter将此结点添加到同步队列的尾部(此时可能会有多个线程结点试图加入同步队列尾部,需要以线程安全的方式添加);
c.该结点在队列中尝试获取同步状态,若获取不到,则阻塞结点线程,直到被前驱结点唤醒或者被中断。
可以看出, 该方法中涉及了四个方法的调用:
(1)tryAcquire(arg)
该方法由继承AQS的子类实现, 为获取锁的具体逻辑。
(2)addWaiter(Node mode)
该方法由AQS实现, 负责在获取锁失败后调用, 将当前请求锁的线程包装成Node扔到sync queue中去,并返回这个Node。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); //将当前线程包装成Node
//指向尾结点tail
Node pred = tail;
// 如果队列不为空, 则用CAS方式将当前节点设为尾节点
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 代码会执行到这里, 只有两种情况:
// 1. 队列为空
// 2. CAS失败
// 注意, 这里是并发条件下, 所以什么都有可能发生, 尤其注意CAS失败后也会来到这里
enq(node); //将节点插入队列
return node;
}
//enq内部是个死循环,通过CAS设置尾结点,不成功就一直重试。很经典的CAS自旋的用法,这是一种乐观的并发策略。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { //如果队列为空,创建结点,同时被head和tail引用
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {//cas设置尾结点,不成功就一直重试
t.next = node;
return t;
}
}
}
}
(3)acquireQueued(final Node node, int arg)
该方法由AQS实现,这个方法比较复杂, 主要对上面刚加入队列的Node不断尝试以下两种操作之一:
- 在前驱节点就是head节点的时候,继续尝试获取锁
- 将当前线程挂起,使CPU不再调度它
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 在当前节点的前驱就是HEAD节点时, 再次尝试获取锁
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) {
//获取前驱结点的wait值
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)//若前驱结点的状态是SIGNAL,意味着当前结点可以被安全地park
return true;
if (ws > 0) {
// ws>0,只有CANCEL状态ws才大于0。若前驱结点处于CANCEL状态,也就是此结点线程已经无效,从后往前遍历,找到一个非 //CANCEL状态的结点,将自己设置为它的后继结点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 若前驱结点为其他状态,将其设置为SIGNAL状态
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//LockSupport.park(this)执行完成后线程就被挂起了,除非其他线程unpark了当前线程,或者当前线程被中断了,否则代码是不会再 //往下执行的,后面的Thread.interrupted()也不会被执行
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//使用LockSupport使线程进入阻塞状态
return Thread.interrupted();// 线程是否被中断过
}
(4)selfInterrupt
该方法由AQS实现, 用于中断当前线程。由于在整个抢锁过程中,我们都是不响应中断的。那如果在抢锁的过程中发生了中断怎么办呢,总不能假装没看见呀。AQS的做法简单的记录有没有有发生过中断,如果返回的时候发现曾经发生过中断,则在退出acquire
方法之前,就调用selfInterrupt
自我中断一下,就好像将这个发生在抢锁过程中的中断“推迟”到抢锁结束以后再发生一样。
从上面的介绍中可以看出,除了获取锁的逻辑 tryAcquire(arg)
由子类实现外, 其余方法均由AQS实现。
2.release()
public final boolean release(int arg) {
if (tryRelease(arg)) {//调用使用者重写的tryRelease方法,若成功,唤醒其后继结点,失败则返回false
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//唤醒后继结点
return true;
}
return false;
}
相比获取锁的acquire
方法, 释放锁的过程要简单很多, 它只涉及到两个子函数的调用:
- tryRelease(arg),该方法由继承AQS的子类实现, 为释放锁的具体逻辑
- unparkSuccessor(h),唤醒后继线程
unparkSuccessor(h)
private void unparkSuccessor(Node node) {
//获取wait状态
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);// 将等待状态waitStatus设置为初始值0
Node s = node.next;//后继结点
//若后继结点为空,或状态为CANCEL(已失效),则从后尾部往前遍历找到一个处于正常阻塞状态的结点进行唤醒
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);//使用LockSupprot唤醒结点对应的线程
}
release的同步状态相对简单,需要找到头结点的后继结点进行唤醒,若后继结点为空或处于CANCEL状态,从后向前遍历找寻一个正常的结点,唤醒其对应线程。
五、共享式
1.acquireShared()
共享式:共享式地获取同步状态。对于独占式同步组件来讲,同一时刻只有一个线程能获取到同步状态,其他线程都得去排队等待,其待重写的尝试获取同步状态的方法tryAcquire返回值为boolean,这很容易理解;对于共享式同步组件来讲,同一时刻可以有多个线程同时获取到同步状态,这也是“共享”的意义所在。其待重写的尝试获取同步状态的方法tryAcquireShared返回值为int。
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
- 当返回值大于0时,表示获取同步状态成功,同时还有剩余同步状态可供其他线程获取;
- 当返回值等于0时,表示获取同步状态成功,但没有可用同步状态了;
- 当返回值小于0时,表示获取同步状态失败。
public final void acquireShared(int arg) {
//返回值小于0,获取同步状态失败,排队去;获取同步状态成功,直接返回去干自己的事儿。
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
doAcquireShared
private void doAcquireShared(int arg) {
//构造一个共享结点,添加到同步队列尾部。若队列初始为空,先添加一个无意义的傀儡结点,再将新节点添加到队列尾部。
final Node node = addWaiter(Node.SHARED);
boolean failed = true;//是否获取成功
try {
boolean interrupted = false;//线程parking过程中是否被中断过
for (;;) {//死循环
final Node p = node.predecessor();//找到前驱结点
if (p == head) {//头结点持有同步状态,只有前驱是头结点,才有机会尝试获取同步状态
int r = tryAcquireShared(arg);//尝试获取同步装填
if (r >= 0) {//r>=0,获取成功
//获取成功就将当前结点设置为头结点,若还有可用资源,传播下去,也就是继续唤醒后继结点
setHeadAndPropagate(node, r);
p.next = null; // 方便GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&//是否能安心进入parking状态
parkAndCheckInterrupt())//阻塞线程
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
大体逻辑与独占式的acquireQueued
差距不大,只不过由于是共享式,会有多个线程同时获取到线程,也可能同时释放线程,空出很多同步状态,所以当排队中的老二获取到同步状态,如果还有可用资源,会继续传播下去。
setHeadAndPropagate
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
2.releaseShared()
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();//释放同步状态
return true;
}
return false;
}
private void doReleaseShared() {
for (;;) {//死循环,共享模式,持有同步状态的线程可能有多个,采用循环CAS保证线程安全
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);//唤醒后继结点
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
六、总结
- 共享锁的调用框架和独占锁很相似,它们最大的不同在于获取锁的逻辑——共享锁可以被多个线程同时持有,而独占锁同一时刻只能被一个线程持有。
- 由于共享锁同一时刻可以被多个线程持有,因此当头节点获取到共享锁时,可以立即唤醒后继节点来争锁,而不必等到释放锁的时候。因此,共享锁触发唤醒后继节点的行为可能有两处,一处在当前节点成功获得共享锁后,一处在当前节点释放共享锁后。