文章目录
1.什么是AQS?
AQS是AbstractQueuedSynchronizer抽象类的简写;AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态的同步器。AQS的核心是自旋(循环)、CAS算法加锁、LocksSouport(阻塞和唤醒) 和队列(公平锁和非公平锁)。
1.1AQS的结构
及AQS的子类图
1.2AQS大致过程
1) tryAcquire(arg)
:锁竞争
2) addWaiter(Node node)
:进入同步等待队列(线程入队
Node:共享属性,独占属性
创建的Node节点有pre(前面节点),next(后面节点),waitestate(节点生命状态),Thread (线程引用).
waitestate:SIGNAL= -1 可被唤醒状态;CANCELLED=1:代表异常,中断引起的,需要废弃结束;
CONDITION= -2:条件等待;PROPAGATE= -3:传播;0::初始化状态;
为了保证所有的阻塞队列线程对象能够被唤醒 compareAndSetTail(t,node) 入队也存在竞争。
3) acquireQueued(final Node node, int arg)
:当前节点线程要开始阻塞
节点阻塞之前还要在尝试一次获取锁;
1能够获取到,节点出队列,并把head向后移动一个节点,新的头节点就是当前节点
2不能获取到,阻塞等待被唤醒
1>首先第一轮循环、修改head的状态,修改为-1标记可被唤醒;
2>第二轮循环,阻塞线程,并且需要判断线程是否有中断信号唤醒的!
shouldParkAfterFailedAcquire(p,node)
waitestate = 0->-1 head节点为什么改到-1,因为持有锁的线程T0在释放锁的时候,需要判断head节
点的waitestate是否!=0,如果!=0成立,会再把waitstate =-1->0,要想唤醒排队的第一个线程T1,T1被唤醒接着走循坏,去抢锁,可能会出失败(在非公平锁场景下),此时可能有线程T3持有了锁!T1可能再次被阻塞。head的节点状态需要再一次经历两轮循环:waitstate=0->-1。
2.AQS源码详解
2.1 tryAcquire 抢锁
非公平尝试获取锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取已经加锁的次数
int c = getState();
// 没有线程持有锁
if (c == 0) {
// 直接抢锁。没有判断队列中是否有线程排队,插队,不公平
if (compareAndSetState(0, acquires)) {
// 抢锁成功
setExclusiveOwnerThread(current);
return true; }
}
// 正在有线程持有锁,并且这个线程是自己(t1)
else if (current == getExclusiveOwnerThread()) {
// t1 已经获取到锁,无需再次获取锁,只需把锁的次数增加即可
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 设置锁的次数
setState(nextc);
return true;
}
return false;
}
公平锁尝试获取
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// hasQueuedPredecessors: 当线程尝试获取锁时,不是直接去抢,
// 而是先判断是否存在队列,如果存在就不抢了,返回抢锁失败
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 是否存在队列并且(下一个待唤醒的线程不是本线程(准备重入锁))
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
为什么需要再次抢锁?
因为抢锁失败有两种原因,1是当前线程确实没有获取到锁。2是当前线程之前已经获取到锁了,还想再获取一次。
对于1这种情况,让线程再抢一次,可能会抢到锁,就不用调用系统api把线程挂起,提高性能。
对于2, 只需改变加锁的次数,就可以标记当前线程已经加锁的次数了,再释放锁时,对应的减成0就可以认为当前线程已经完全释放锁了,这就是锁的可重入实现原理。
2.2 addWaiter进入等待队列
private Node addWaiter(Node mode) {
// 以当前线程为参数,构造一个新的 node,记作当前线程节点
Node node = new Node(Thread.currentThread(), mode);
// 在最开始,tail 和 pred 肯定都是null,
Node pred = tail;
// 最开始不会进入下面,只有队列不为空时,才会进入
if (pred != null) {
node.prev = pred;
// 将节点加入队尾
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 而是由 enq(node) 构造节点
enq(node);
return node;
}
private Node enq(final Node node) {
// 开始了循环
for (;;) {
Node t = tail;
// 最开始队列是空的。只有第一次循环会进入
if (t == null) { // Must initialize
// 构造了一个空的node节点当作队列的头节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 第二次及后面的循环会走到这里
// 先设置当前节点的前驱节点是 队尾节点。
node.prev = t;
// 用CAS算法把当前节点 设置成队尾
if (compareAndSetTail(t, node)) {
// 这样上一次的队尾t就不是队尾了,t 就有了后继节点node
t.next = node;
return t;
}
}
}
}
经过addWaiter(Node node)
方法后,队列中至少存在两个节点,第一个就是必须的空节点,不包含线程信息,第二个才是真正待执行的线程节点。
2.3 acquireQueued 线程阻塞
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中的线程信息,和初始化时设置的空头节点一样
setHead(node);
// 断开前驱节点,旧的 head 会被垃圾回收
p.next = null; // help GC
failed = false;
return interrupted;
}
//走到这里说明不是头节点,或者抢锁失败
// shouldParkAfterFailedAcquire(p, node):
// 检查 node 是否是可唤醒的(waitStatus == -1),如果是,返回true
// 如果node不是可唤醒的,并且node没有被取消掉,则将node设置设置为可唤醒,返回false,
// 下一次循环时就会返回false
// parkAndCheckInterrupt(): 挂起线程
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 这个判断不会走,可以认为 failed 和 interrupted 标识这里无用。
// 程序能走到这里,说明 (p ==head && tryQcquire(arg)) 为true,那么 failed 和 interupted 恒为false
// 否则就会陷在循环中,无法到 finally 中。
if (failed)
cancelAcquire(node);
}
}
// 把node设置为队列的头节点
private void setHead(Node node) {
head = node;
// 清空了线程信息
node.thread = null;
node.prev = null;
}
shouldParkAfterFailedAcquire(Node pred, Node node)
// 接受两个参数,一个是当前节点的前驱节点,一个是当前节点
/**
* 这里使用前驱节点中的waitStatus状态来判断当前节点是否可以被唤醒。
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 前驱节点的状态
int ws = pred.waitStatus;
// 如果是可唤醒的,直接返回true
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
// 标识前驱节点已经取消锁竞争,跳过这个前驱节点,继续向前查找
do {
// 一直向前找
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0); // 到不是已取消的节点为止
// 设置有效的前驱节点
pred.next = node;
} else {
// 将前驱节点的 ws 设置可唤醒的
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
2.4 release 释放锁
- 减少加锁的次数(state),如果state == 0, 代表当前线程可以释放锁,然后把持有锁的线程标记为空
- 唤醒队列中第一个待运行的线程也就是第二个节点,因为第一个节点是当前已获取到锁正在运行线程
public final boolean release(int arg) {
// 释放锁
if (tryRelease(arg)) {
Node h = head;
// 头节点不为空,且头节点的waitStatus不是默认状态
if (h != null && h.waitStatus != 0)
//传入的是头节点
unparkSuccessor(h);
return true;
}
return false;
}
// 释放锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
// 再次将ws 置为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);
}