1.口口相传的AQS到底是什么
AQS的完整类名是——AbstractQueuedSynchronizer(直译过来就是抽象队列同步器)AQS为Java中几乎所有的锁和同步器提供一个基础框架,派生出如ReentrantLock、Semaphore、CountDownLatch、线程池中的worker等。本文基于AQS原理的几个核心点
1.AQS如何定义资源:
- Exclusive-独占,只有一个线程能执行,如ReentrantLock
- Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch
2.两种队列
- 同步等待队列: 主要用于维护线程获取锁失败时入队的线程
- 条件等待队列: 线程在调用await()的时候会释放锁,然后线程会加入到条件等待队列,线程在调用signal()唤醒的时候会把条件队列中的线程节点再次移动到同步队列中,并且等待再次获得锁
3.四大特性
- 阻塞等待队列
- 共享/独占
- 公平/非公平
- 可重入
- 允许中断
3.五种状态
- 值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。
- CANCELLED,值为1,表示当前的线程被取消;
- SIGNAL,值为-1,处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行
- CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;
- PROPAGATE,值为-3,共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点;
4.关键方法
不同的自定义同步器竞争共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
2.state状态如何维护
/**
* The synchronization state.
*/
private volatile int state;
/**
* Returns the current value of synchronization state.
* This operation has memory semantics of a {@code volatile} read.
* @return current state value
*/
protected final int getState() {
return state;
}
/**
* Sets the value of synchronization state.
* This operation has memory semantics of a {@code volatile} write.
* @param newState the new state value
*/
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
注意下面三点:
- state用volatile修饰,保持在线程中的可见性
- getState和setState用final修饰,表示该方法在子类不可被覆盖
- compareAndSwapInt使用CAS的思想来交换数据
3.CLH队列
CLH实际上是一个FIFO双向队列,队列中元素的类型为Node, Node里面封装的对象就是线程,AQS其实是基于这个队列来完成对同步状态state的管理,假设线程获取同步状态失败时,会把线程的相关信息封装成Node节点加入到CLH队列中,同时会阻塞当前线程。
CLH队列模型图
1.node节点内部结构
CLH同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next),condition队列的后续节点(nextWaiter)
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 Thread thread;
//用于condition里面调用
Node nextWaiter;
}
2.入队方法
1.acquire(int arg)
入口处统一调用acquire方法来获取锁
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
在tryAcquire方法中,AQS是没有实现的,这块留给子类去实现,这块子类实现的一个典型的例子就是ReentrantLock的公平锁和非公平锁的实现,后续这乱
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
2.addWaiter(Node mode)
private Node addWaiter(Node mode) {
//把前端的线程包装成节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 在初始的情况下,pred = tail=null
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//CAS循环插入节点
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
//当tail不存在,其实就是首次插入时,会放入一个空节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
//把尾节点由t节点变为node节点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
3.acquireQueued(final Node node, int arg)
下面的代码是插入队列,队列中等待的节点不会被中断。
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);
}
}
对于可中断的插入节点
/**
* Acquires in exclusive interruptible mode.
* @param arg the acquire argument
*/
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//这块最大的区别,如果是中断,直接会抛出异常
throw new InterruptedException();
}
} finally {
//当抛出异常时,取消节点
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
//如果前端的节点被取消,那么会一直向前面查找,并且把当前的节点挂在前 面的未取消的节点
//之后
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
//如果当前状态没设置,会设置成SIGNAL
//表示当前节点的后续节点通过park阻塞了
//这块会修改前驱节点的状态
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
//返回线程是否中断
return Thread.interrupted();
}
3.出队方法
1.release(int arg)
public final boolean release(int arg) {
//修改状态
if (tryRelease(arg)) {
Node h = head;
//head=null的情况只有一个线程进入,没有初始化队列,!=null至少说明队列被初始化过,但是是否
//有后续节点未知,waitStatus!=0说明下个节点是等待的
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
2.unparkSuccessor(Node node)
unparkSuccessor环境之后阻塞监听的节点
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
//修改node节点的状态
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
//waitStatus>0表示下个线程是被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);
}
4.Condition源码
- Condition#await方法会释放当前现成持有的锁,并且会阻塞当前线程,同时向Condition队列尾部添加一个节点,所以调用Condition#await方法的时候线程必须持有锁。
- Condition#signal方法会把Condition队列的首节点移动到阻塞队列尾部,并且会唤醒调用Condition#await方法而阻塞的线程(此处要注意:唤醒之后这个线程就可以去竞争锁了),所以调用Condition#signal方法的前提是必须持有锁。
1.await()方法
public final void await() throws InterruptedException {
//若当前线程已经中断则抛出中断异常
if (Thread.interrupted())
throw new InterruptedException();
//封装当前的节点,并且添加到等待队列
Node node = addConditionWaiter();
//释放当前线程所占用的锁,并保存当前锁的状态
int savedState = fullyRelease(node);
int interruptMode = 0;
//
while (!isOnSyncQueue(node)) {
//如果当前node节点不在同步队列中,进行阻塞
LockSupport.park(this);//这一行代码执行,当前线程就阻塞住了
//执行到这一步说明当前线程被唤醒了
//有可能是线程中断被唤醒
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//acquireQueued方法中,如果是队列的第一个等待节点,会再次争夺锁,否则插入到队列之中
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
//interruptMode == REINTERRUPT,则设置线程中断标识
//interruptMode == THROW_IE 抛出线程中断异常
reportInterruptAfterWait(interruptMode);
}
2.signal()方法
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//获取第一个等待者
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
//不断沿着单向列表进行循环遍历
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
//修改线程节点的状态
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
//插入到AQS队列之中
Node p = enq(node);
int ws = p.waitStatus;
//切换到可唤醒的状态
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
5.为什么AQS同步队列要使用双向链表
AQS使用双向链表主要由以下3个原因
- 在没有竞争到锁的线程加入到阻塞队列的时候,需要判断前面节点是否是正常的状态,主要为了防止存在异常线程导致后续线程无法唤醒的问题。
- 当lock接口里面有一个lockInterruptibly()方法,这个方法表示在处于阻塞的情况是允许被中断的,当被中断的时候会把当前的节点修改为cancel状态,但是这个节点仍然存在列表中,意味想着在后续的锁竞争中,需要把这个节点从列表中删掉,假设此刻这个列表是单向列表,则新插入的节点需要从头遍历,性能很慢。
- 为了避免竞争的开销,对于加入到队列的队列的节点需要判断前面的是否是头节点,如果是头节点的下一个节点才有必要再次获取锁,否则,会插入到节点的尾部,这种可以避免羊群效应,如果是单向列表,需要从第一个节点去查找,会造成性能的损耗。