AQS:AbstractQueuedSynchronizer, 队列同步器,它是Java并发用来构建锁和其他同步组件的基础框架
官方文档:提供一个框架,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量,事件等)。 该类被设计为大多数类型的同步器的有用依据,这些同步器依赖于单个原子int
值来表示状态。主要是通过维护一个state 变量以及用来存放阻塞线程的双向队列来控制同步
state 0代表释放,1或者>1(锁重入)代表被占有,
从ReentrantLock的非公平锁的lock实现来看AQS:
一开始没想写源码分析,总感觉少点什么,补上之后,结合流程图和AQS队列流程还是比较容易理解的,请耐心看源码分析的注释
对着源码看更容易理解,难于理解的在于对AQS双向队列的操作:
AQS双向队列操作:假设ABC三个线程,A先获取到锁
锁释放:
ReentrantLock源码分析:
//非公平锁NonfairSync
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
//lock入口
final void lock() {
//CAS 直接去获取锁,尝试修改state 0变为1。修改成功则获取锁成功。
//这里用CAS 修改成功返回true,修改失败说明state已经不是0,被其他线程抢先修改了,返
//回false
if (compareAndSetState(0, 1))
//成功获取锁,设置持有锁的线程为自己也就是当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
//CAS获取所失败,再次尝试获取锁,如果锁已经释放,这步就会有很大的性能提高
//这里是父类的抽象,子类实现就是下面的tryAcquire
acquire(1);
}
//非公平锁尝试获取锁
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
下面看CAS获取锁失败的逻辑,进入acauire(1):
//AQS尝试获取锁
public final void acquire(int arg) {
//tryAcquire需看子类的具体实现,这里看的是NonfairSync的tryAcquire
//实现nonfairTryAcquire
if (!tryAcquire(arg) &&
//尝试获取所失败,会为当前线程建立线程节点放入队列尾走addWaiter(Node.EXCLUSIVE)
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//中断补偿
selfInterrupt();
}
//NonfairSync的tryAcquire实现
//尝试获取锁,true获取锁成功,false失败
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//state=0可以进行获取锁
if (c == 0) {
if (compareAndSetState(0, acquires)) {
//CAS成功获取锁,设置持有锁的线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
//c!=0说明锁已经被持有,判断是已经持有锁的线程,(可重入)
else if (current == getExclusiveOwnerThread()) {
//可重入锁 state+1
int nextc = c + acquires;
//加1之后小于0,state值不对抛出异常
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//可重入锁加1后赋值
setState(nextc);
return true;
}
return false;
}
//新建线程节点,mode以给定模式构造结点,有两种:EXCLUSIVE(独占)和SHARED(共享)这里是独占
//ReentrantLock为独占锁
private Node addWaiter(Node mode) {
//为当前线程建立节点,独占模式,当前线程也就是刚刚来尝试获取锁失败的线程
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//这里想把新建的节点放入队尾,所以当前队尾的next 应指向新建的节点
Node pred = tail;
//当前队尾不为空,就是说当前队列里有没有其他线程节点在等待
if (pred != null) {
//新建节点的前置指向当前队尾节点,
//这里就像流程图里,放入C线程节点的时候,C的前置指向B
node.prev = pred;
//然后CAS更新尾巴节点为新建节点
if (compareAndSetTail(pred, node)) {
//更新成功,把队列的当前队尾节点的下一个节点指向新建节点
//因为是双向队列,要改变或确定一个节点的位置 就要指定节点的prev和next
//就是我在你后面,我的前面是你,你的候面是我,才能确定我在你后面
//这里有个问题,因为分三步操作:1 修改新建节点的前置。2 CAS修改尾巴节点tail
//3 修改新建节点的前置节点的后置,三步操作不是原子操作,思考点,有可能我
//的前置节点修改好了,但是我的前置节点的后置却是null--就是我知道我前面是你,
//你不知道你后面是我
pred.next = node;
//返回新建节点
return node;
}
}
//这里如果尾巴节点为null,说明目前还没有线程节点在队列里,需初始化头节点,并把新建节点
//放在头节点的候面
//这里可以看流程图的,B线程节点入队的时候,
enq(node);
return node;
}
//初始化头节点,并把新建的线程节点放入头节点候面。可看流程图B节点入队的时候
private Node enq(final Node node) {
//这个是死循环直到新建节点放入队列尾成功,跳出循环
for (;;) {
//再次判断尾巴节点是不是空,因为可能中途被其他线程新建了
Node t = tail;
if (t == null) { // Must initialize
//CAS创建头节点
if (compareAndSetHead(new Node()))
//初始化头节点也是尾巴节点
tail = head;
}
//头节点被初始化之后,下次循环走这个
else {
//这块和头节点不为空的操作是一样的,新建节点的前置指向尾巴节点
node.prev = t;
//CAS 更新尾巴节点为新建节点
if (compareAndSetTail(t, node)) {
//原来的尾巴节点的后置指向新建节点
t.next = node;
//返回新建节点
return t;
}
}
}
}
新建节点放入队尾之后,看接下来的操作,分析acquireQueued方法,这个方法是我认为不太好理解的地方,包括线程自旋获取锁,
获取失败之后标记前置节点的waitStatus为SIGNAL,然后自身park 停止自旋,前置节点释放掉锁之后,unpark唤醒后置最近的有效节点(实际处理起来并不像说的那么简单),看Doug Lea大师怎么处理的:
//新入队列的节点都会走这一步,从流程图线程B节点入队来分析
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//死循环也就是自旋,这里每个节点至少自旋一次去获取锁,获取失败后线程走else,会让线程
//park,park后就不再自旋了,等待unpark唤醒
for (;;) {
//获取新建节点的前置
final Node p = node.predecessor();
//如果新建节点的前置为头节点,那么新建节点会再次尝试获取锁
//如果此时锁被释放,成功获取锁,
if (p == head && tryAcquire(arg)) {
//设置新的头节点为新建节点,
//流程图的B线程节点成功获取锁,头节点被放弃,
//新的头节点为B
setHead(node);
//让原来的头节点指向为null ,GC会回收掉。头节点prev本身就是null
p.next = null; // help GC
failed = false;
return interrupted;
}
//1.如果说B线程节点获取锁失败
//2.或者说新建节点的前置不是头节点,可看流程图线程C线程节点入队,他的前置
//节点是B节点
//如果获得锁失败,根据waitStatus决定是否park线程
if (shouldParkAfterFailedAcquire(p, node) &&
//前面执行为true会执行这一步park线程,等待unpark唤醒后,
//检查线程节点线程是否被中断,被中断则返回true
//如果被中断过,阻塞中的线程并不会响应中断,回到acquire()看到
//selfInterrupt() 补偿中断
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//抛出异常,取消节点操作,出队操作,这块没啥好说的
if (failed)
cancelAcquire(node);
}
}
//主要把当前节点的前置节点waitStatus标记为SIGNAL,释放锁后好唤醒当前节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//如果为SIGNAL也就是-1 说明已经被标记过,直接返回
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
//waitStatus>0说明前置节点是已经撤销无效的节点,跳过
//拿流程图的BC来说明,BC都在等待,然后突然B不想等待了,撤销了,那么由谁来唤醒C,
//C就往前继续找有效的前置节点,找到了头节点
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;
}
//这里如果waitStatus没有被修改过,waitStatus默认为0
//或者说小于0又不是SIGNAL,也就只有PROPAGATE
//PROPAGATE在共享锁以及condition队列里使用,这里不做介绍
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.
*/
//CAS 更改前置节点的waitStatus为SIGNAL,这样前置节点释放锁后会唤醒当前节点node
//这里就会发现,从AQS队列操作流程图来看,B节点把A节点的waitStatus置为SIGNAL,
//C节点把B节点的waitStatus置为SIGNAL,这样A节点释放锁,会唤醒B,B执行完释放锁会
//唤醒C
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
加锁到这里就结束了。下面看锁的释放,锁释放比较简单:
//释放锁
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
//成功释放锁后,判断队列中是否有需要唤醒的线程节点
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);
}
//更新state ,这里如果state =0释放锁成功,如果不等于0,
//还有资源没释放(可重入锁机制)
setState(c);
return free;
}
//唤醒节点
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)
//这里的ws 一般是-1
//修改当前节点的waitStatus为0就是我可以唤醒别人了
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;
//当前节点后置没有节点或者节点无效
if (s == null || s.waitStatus > 0) {
s = null;
//从队列尾部往前遍历寻找当前节点最近的有效节点
//这里为什么从后往前遍历,因为在加锁的时候,新建节点的时候那个思考?
//回到addWaiter看,三步不是原子操作。有可能头节点的next节点为空。但是其实已经存在了
//只是还没有做pred.next = node这一步,所以从后往前遍历一定会遍历到所以节点
for (Node t = tail; t != null && t != node; t = t.prev)
//找到唤醒的节点t
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒s线程继续可以自旋争夺锁
LockSupport.unpark(s.thread);
}
到这里就结束了:分析过程中遇到几个问题。查了很多:park 使当前线程取消自旋(即使在死循环里),一开始对为什么释放锁的时候寻找后置节点从后遍历有疑问,因为感觉从前遍历一下不就找到了吗,分析完之后发现了原因。
这里再说下简述下公平锁吧,公平锁和非公平锁不同就是,公平锁不会直接CAS获取锁,而是先去队列里判断有没有等待的线程,如果有等待的线程,那么直接排在候面,如果只有头节点或者没有等待的线程他会尝试获取锁,主要看hasQueuedPredecessors这个方法,其他的都和公平锁一样了
AQS里面使用了大量的CAS和unsafe类的方法,有兴趣可以了解一下,底层C++实现的
AQS的静态内部类Node 也可以具体了解下
AQS里还有很多没分析到,有在像Semaphore,CountDownLatch里面用到的,后续再去学习,关于AQS 网上太多文章了,我这里就当作自己的一次学习笔记吧