java aqs
AQS
-
全称AbstractQueuedSynchronizer,即队列同步器,它就是Java的一个抽象类,他的出现是为了解决多线程竞争共享资源而引发的安全问题,细致点说AQS具备一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制aqs是用clh队列锁实现的,即将暂时获取不到锁的线程加入到队列中,队列是双向队列。
-
AQS是一个抽象类,主要是通过继承方式使用,本身没有实现任何接口,仅仅是定义了同步状态的获取和释放的方法。
-
ReentrantLock、ReentrantReadWriteLock、Semaphore,CountDownLatch等都是基于AQS
独占锁源码分析
此处以ReentrantLock为例
lock()
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
//调用父类acquire,尝试获取
final void lock() {
// 父类方法,1表示一次
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取锁的状态,其实就是计数器,0表示锁没有被任何线程获得
int c = getState();
if (c == 0) {
// 1.hasQueuedPredecessors 有没有前序结点,如果有肯定轮不到当前线程,
//2.compareAndSetState 设置锁计数器=1,原子操作
//3.setExclusiveOwnerThread 设置独占
// 三个操作都符合条件才算当前线程获得锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 如果当前线程已经获得锁,那么锁计数器state加1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
总结:AQS获取锁的机制就是维护一个int属性state
- state=0 表示锁没人再用
- state>0 表示有线程获取,state的值表示线程重入的次数
acquireQueued()
这块是核心代码
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//死循环,直到return interrupted获取锁
for (;;) {
/*
1.这个死循环的作用是如果当前线程还轮不到获取锁,则进入队列,并且当前线程中断(后边调用了LockSupport.park())
2.如果当前线程节点的上一个节点是head节点,则不断尝试去获取锁
*/
//获取前任节点
final Node p = node.predecessor();
//如果前任节点是head节点,则尝试获取锁,此时属于第二次获取锁,第一次是lock时便会尝试获取锁
if (p == head && tryAcquire(arg)) {
//获取到锁后,把head节点指向当前node
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// shouldParkAfterFailedAcquire返回true表示前序节点还在排队,所以当前节点需要去park,进到parkAndCheckInterrupt方法
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
// 如果上面代码没有获取锁报错,需要取消获取锁的动作
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire()
//获取锁失败后应该排队
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//走进这个方法说明前边没有获取到锁,即tryAcquire()失败
// 先拿到前序节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 前序节点还在排队呢,所以当前节点node只能挂起,安心地去排队
// 这里说下节点得SIGNAL状态,它的意思是如果锁被释放,应该通知SIGNAL状态的节点
return true;
// 下面的情况,node节点不需要去park,最终返回false使上层调用方法死循环直到获取锁
// 首先是ws>0,即waitStatus=cancelled=1(看内部类:Node)
if (ws > 0) {
// 如果先序节点的状态是取消,则把异常的先序节点从队列中删除
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 走到这里,前序节点的waitStatus只能是0/-3,
// 把前序节点的waitStatus设置为-1:SIGNAL,因为啥呢?
// 看上面if (ws == Node.SIGNAL)分支,只有前序节点waitStatus=-1,当前节点才能安心地去队列等待,
// 否则当前node会一直自旋获取锁,这显然是不合理的
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
- 这里我们需要注意,当执行完LockSupport.park(this)后,此线程就被挂起了,除非当其他线程调用LockSupport.unpark唤醒当前线程或者当前线程被中断,否则后面代码是不会执行的
- 假设此时线程被唤醒了,我们此时是不知道线程是被unpark方法还是中断唤醒的,所以我们需要通过Thread类的interrupted方法来判断。interrupted方法会返回给我们当前线程的中断标志位,并将中断标志位复位,即置为false。如果我们是中断唤醒的,则返回true,然后会进入acquireQueued的第二个If分支中将interrupted置为true。然后再次进入for循环自旋,看是获取锁还是又被挂起。
Node.waitStatus
AQS使用FIFO双向阻塞队列来保存被阻塞的线程,实现机制是,AQS通过其内部类Node封装线程,同时Node维护prev,next,waitStatus信息来实现双向队列;
针对节点的waitStatus属性(等待状态),要补充说明一下,它的取值有以下几种:
- CANCELLED = 1; // 取消状态,唯一个大于0的状态,表示节点获取锁超时。例如:1,2,3三个节点按顺序获取锁,结果1正在处理业务,2,3排队等待,2先来的,等久了超时了,而3没超时,等1释放锁以后,3虽然排在2后面,但是会把CANCELLED状态的2节点删掉,让后面未取消的节点顶上来;
- SIGNAL = -1; // 等待触发状态,这个状态的节点就是锁释放后需要被通知的节点;
- CONDITION = -2; // 当其他线程调用了condition的signal方法后,condition状态的节点会从等待队列转移到同步队列中,等待获取同步锁
- PROPAGATE = -3; // 共享模式下,前驱节点不仅会唤醒其后继节点,同时也可能唤醒后继的后继节点。
- 0;新节点入队时候的默认状态。
AbstractQueuedSynchronizer 结构
加锁流程总结
独占锁线程唤醒
release
// 首先是尝试释放锁,有人问了,这释放锁还需要尝试吗?又不是获取锁,还可能获取不到
// 那确实存在释放不了的情况,什么情况呢? 那就是重入次数大于1的情况,按照重入锁的设计,重入几次就需要释放几次
public final boolean release(int arg) {
if (tryRelease(arg)) {
// 锁释放成功,要唤醒后序挂起线程
Node h = head;
// 判断head节点状态非0,即被修改过
if (h != null && h.waitStatus != 0)
// 如果h.waitStatus = 0,表示没有后续节点,能理解不?看上面第三点
unparkSuccessor(h);
return true;
}
return false;
}
unparkSuccessor
// 注意参数是head节点,因为唤醒后续节点总是从head往后找
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
// 还原head节点状态为0
compareAndSetWaitStatus(node, ws, 0);
// 找到后序节点
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);
}
共享锁源码分析
此处以ReentrantReadWriteLock为例,
acquireShared
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;
for (;;) {
//前驱节点
final Node p = node.predecessor();
if (p == head) {
//若是前驱节点为头结点,则尝试获取同步状态
int r = tryAcquireShared(arg);
if (r >= 0) {
//获取同步状态成功
//修改头结点,并传递唤醒状态
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
//补中断
selfInterrupt();
failed = false;
return;
}
}
//与独占锁一致
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
tryAcquireShared(arg) 返回值表示当前可用的资源。
setHeadAndPropagate
private void setHeadAndPropagate(Node node, int propagate) {
//propagate == 0 表示没有资源可以使用了
Node h = head; // Record old head for check below
//设置头结点
setHead(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();
}
除了将头结点指向当前节点外,还需要唤醒下一个共享节点。
而独占锁不会。
读写锁(待补充)
相关概念
CLH队列
CLH队列(Craig, Landin and Hagersten queue)是一种基于链表结构的线程等待队列,由三位计算机科学家Maurice Craig、Colin Landin和C. R. Hagersten在1971年提出。该队列被广泛应用于多线程环境下的自旋锁和其他同步原语实现中。
在CLH队列模型中,当多个线程尝试获取一个资源时,竞争失败的线程不会立即进入阻塞状态,而是将自身放入一个特殊的链表(通常称为FIFO等待队列)。每个线程在队列中有一个对应的节点,节点上包含了一个标志位或称为”锁定“字段,用于表示该线程是否已经释放了锁。
当线程尝试获取资源时:
- 如果资源可用,则线程获取资源并执行相应操作。
- 如果资源已被其他线程占用,则线程构造一个新的节点,并将其添加到CLH队列的尾部,然后开始循环检查其前驱节点的状态(即检查前驱节点的“锁定”字段)。
- 当前线程的前驱节点释放资源后会修改其“锁定”字段,这样当前线程就能感知到资源已释放,有机会再次尝试获取资源。
这种设计使得线程在等待资源的过程中可以继续运行(通过自旋),而不是直接进入阻塞状态等待唤醒,从而减少上下文切换带来的开销,提高了并发环境下的性能表现。同时,CLH队列能够确保线程按照先进先出(FIFO)的原则进行调度,避免了死锁和饥饿问题的发生。