AQS(AbstractQueuedSynchronizer)即抽象队列同步器,是一套可以实现同步锁机制的框架,是许多JUC内同步框架的基石。AQS通过一个FIFO的队列维护线程同步状态,实现类只需要继承该类,并重写指定方法既可以实现一套线程同步机制。
简单的说,AQS维护了一个volatile int state
变量和CLH(三个人名字的缩写)
双向队列,和一个ConditionObject(后续说明)组成
/**
* The synchronization state.
*/
private volatile int state;
AQS中提供了获取和设置state的实现:
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
static final class Node {
volatile int waitStatus; //节点状态
volatile Node prev; //双向链表前驱节点
volatile Node next; //后继节点
volatile Thread thread; //引用线程,头结点不包含线程
Node nextWaiter; //条件队列
}
waitStatus有五种取值:
CANCELLED = 1
。节点引用线程由于等待超时或被打断时的状态。SIGNAL = -1
。后继节点线程需要被唤醒时的当前节点状态。当队列中加入的后继节点被挂起(block)
时,其前驱节点会被设置为SIGNAL
状态,表示该节点需要被唤醒。CONDITION = -2
。当节点线程进入condition
队列时的状态。(见ConditionObject
)PROPAGATE = -3
。仅在释放共享锁releaseShared
时对头节点使用。(见共享锁分析)0
。节点初始化时的状态。获取锁失败的线程会被包装为节点,加入CLH双向队列中,结构如下:
在此之前,我们先举一个例子:
/**
* @author 我见青山多妩媚
* @date Create on 2023/3/13 21:56
*/
@Slf4j
public class Mutex extends AbstractQueuedSynchronizer {
//独占锁加锁
@Override
protected boolean tryAcquire(int arg) {
return compareAndSetState(0,1);
}
//独占锁解锁
@Override
protected boolean tryRelease(int arg) {
return compareAndSetState(1,0);
}
//共享锁加锁
@Override
protected boolean tryRelease(int arg) {
return compareAndSetState(1,0);
}
//共享锁解锁
@Override
protected int tryAcquireShared(int arg) {
return super.tryAcquireShared(arg);
}
}
其中compareAndSetState()
方法:
是一个CAS操作来确定更改值的
我们Mutex类中写一个main方法,用来测试独占锁
public static void main(String[] args) {
Mutex mutex = new Mutex();
Thread t1 = new Thread(()->{
log.debug("t1尝试获取锁");
mutex.acquire(1);
log.debug("t1获取锁成功");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("t1准备释放锁");
mutex.release(1);
log.debug("t1锁已释放");
},"t1");
Thread t2 = new Thread(()->{
log.debug("t2尝试获取锁");
try {
//确保t1第一个获取锁
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
mutex.acquire(1);
log.debug("t2获取锁成功");
mutex.release(1);
log.debug("t2锁已释放");
},"t2");
t1.start();
t2.start();
}
运行结果:
17:52:42.623 [t1] DEBUG com.JUCTest.LockTest.AQSTest.Mutex - t1尝试获取锁
17:52:42.623 [t2] DEBUG com.JUCTest.LockTest.AQSTest.Mutex - t2尝试获取锁
17:52:42.626 [t1] DEBUG com.JUCTest.LockTest.AQSTest.Mutex - t1获取锁成功
17:52:45.632 [t1] DEBUG com.JUCTest.LockTest.AQSTest.Mutex - t1准备释放锁
17:52:45.632 [t1] DEBUG com.JUCTest.LockTest.AQSTest.Mutex - t1锁已释放
17:52:45.632 [t2] DEBUG com.JUCTest.LockTest.AQSTest.Mutex - t2获取锁成功
17:52:45.632 [t2] DEBUG com.JUCTest.LockTest.AQSTest.Mutex - t2锁已释放
首先可以肯定是的,t1一定先获取锁,并且在3s后t2才能获取锁
AQS中提供了获取独占锁的方法acquire()
和释放锁的方法release()
acquire()
public final void acquire(int arg) {
//tryAcquire() 实现类设置的值
//如果获取锁失败
if (!tryAcquire(arg) &&
//acquireQueued 尝试找一个节点加锁,否则挂起
//addWaiter() 用来加入节点到队列中
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//如果阻塞线程被打断,抛出异常
selfInterrupt();
}
整个方法的执行流程为:
流程分析:
private Node addWaiter(Node mode) {
//将当前线程封装为节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//tail 为指向尾结点的一个节点
Node pred = tail;
if (pred != null) {
//如果尾结点不为空(队列不为空),那么将node连接到队列中
node.prev = pred;
//compareAndSetTail 通过CAS尝试更改尾结点(tail)为当前节点node
if (compareAndSetTail(pred, node)) {
//成功后,彻底连接到队列中
pred.next = node;
return node;
}
}
//修改tail为node节点,enq为修改tail的唯一方法,内部使用 自旋+CAS实现
enq(node);
return node;
}
addWaiter
将线程包装为独占节点,尾插式加入到队列中,如队列为空,则会添加一个空的头节点,内容也为null
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)) {
//将当前节点设置为头节点
//该方法中,会将head指向node节点,并且将该节点的前驱节点和Thread参数置空
//目的是为了GC help GC
setHead(node);
//将当前节点的下一个节点引用设置为null,方便垃圾处理器回收,下文讲
p.next = null; // help GC
//防止进入finally
failed = false;
//返回被打断状态
return interrupted;
}
//将前序节点设置为挂起(park),挂起没有获得到锁的节点
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//尝试获取锁失败,取消获取锁
if (failed)
cancelAcquire(node);
}
}
意思是,当我们节点进来时,先看看当前队列有没有节点在处理,如果有就去排队,然后等待(被挂起,等快到自己的是被唤醒),如果你不是排队的第一节点,那么直接挂起。当被挂起时,他的waitStatus
被设置为SIGNAL(-1)
,表示需要唤醒,随后通过park进入阻塞状态
另外,源码中有两段help GC,这里设置为空的原因是,头节点不参与排队,因为他已经获取到了同步状态,现在是需要进行业务逻辑操作的,而在业务逻辑操作完之后,该头结点肯定需要进行垃圾回收,防止空间浪费,这里就涉及到GC Root,如果还有对象引用的话,垃圾回收器是不会回收他的,所以要将他的属性置空,方便垃圾回收
当每次线程调用时都会先调用tryAcquire
,失败后才会挂载到队列,因此acquire
实现默认为非公平锁
release()
释放锁的过程比较简单
public final boolean release(int arg) {
//通过自定义的方法,尝试释放锁
if (tryRelease(arg)) {
//释放成功后,指向头节点
Node h = head;
if (h != null && h.waitStatus != 0)
//如果当前头节点不为空,并且不为就绪状态,那么唤醒头节点
//如果头结点的下个节点为空或头结点的waitStatus > 0,
//那么将从尾结点往前遍历,找到最后一个waitStatus<0的节点,将其唤醒(unpark)
unparkSuccessor(h);
return true;
}
return false;
}
从头结点开始唤醒后继节点,
加锁方法和独占锁类似,
public final void acquireShared(int arg) {
//如果小于0,那么和独占锁一样,开始对立面节点进行自旋+CAS ,
if (tryAcquireShared(arg) < 0)
//当前是写锁时,获取锁失败,往下走 通过CAS + 自旋获取锁,因为上一个锁总该要释放的
doAcquireShared(arg);
}
区别是,独占锁返回的是bool,共享锁是int
private void doAcquireShared(int arg) {
//获取锁失败后,以Node.SHARED挂载到队列尾部
final Node node = addWaiter(Node.SHARED);
//其余和独占锁差不多 都是CAS+自旋
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//拿前一个节点
final Node p = node.predecessor();
//如果前一个节点是头节点
if (p == head) {
//那么试着获取共享锁,返回共享锁得到的值
int r = tryAcquireShared(arg);
if (r >= 0) {
//将当前节点和r传入,r表示当前有多少已经获得锁的线程数
//这个方法将单独摘出来讲
//唤醒下一个节点的线程 (共享锁的传播)
setHeadAndPropagate(node, r);
//和独占锁一样,帮助GC的
p.next = null; // help GC
//如果被挂起的节点没结束到这,进行打断
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//和独占锁一样,挂起没有获得到锁的节点
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally { //和独占锁一样
if (failed)
cancelAcquire(node);
}
}
因为这是个自旋,所以会传递唤醒后续的阻塞节点
我们在源码内说的,将setHeadAndPropagate
单独摘出来说
private void setHeadAndPropagate(Node node, int propagate) {
//获取头节点
Node h = head; // Record old head for check below
//将node设置为头结点
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
//注释的意思是,当发生以下情况之一,将给头结点(node)的下一个节点发信号
//如果当前节点是共享锁节点,那么往下可以释放锁
//相当于检查是否有需要释放锁的节点
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
//获取下一个节点,传播释放锁
Node s = node.next;
//如果为空,或者为共享模式,将释放锁
if (s == null || s.isShared())
//释放锁,因为是在自旋内,所以可以唤醒后续多个节点
doReleaseShared();
}
}
源码内的注释写的很详细
因为在这里面,是获取到node.next的节点,所以实际是共享锁的传播解锁(如果条件合适)
共享锁的解锁也和上面的类似,不过独占锁的解锁和加锁都是bool类型,共享锁只有加锁时int类型,更方便控制共享锁的数量吧
public final boolean releaseShared(int arg) {
//如果获取到释放锁
if (tryReleaseShared(arg)) {
//那么进行释放
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
//喜闻乐见 自旋
for (;;) {
//获取投节点
Node h = head;
//不为空,并且头结点后还有节点
if (h != null && h != tail) {
//获取头结点状态
int ws = h.waitStatus;
//如果是被阻塞
if (ws == Node.SIGNAL) {
//CAS尝试设置为0,恢复正常,如果不能设置,继续自旋(如果是已经阻塞的节点,不让他阻塞)
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//类似独占锁的解锁,可以参考,唤醒后继
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) //如果已经是初始化状态,CAS更改
continue; // loop on failed CAS
}
//如果头部改变了,那么继续循环,否则退出
if (h == head) // loop if head changed
break;
}
}
解锁和独占锁的解锁也类似,释放资源后唤醒后继,