本文读一下AQS的源码。
本文理解AQS原理,对之后并发编程有大用。只分析独占式锁,关于共享锁放在读写锁分析。结合ReenTrantLock一起理解。
首先看一下ASQ的API
独占式:
void acquire(int arg):独占式获取同步状态,如果获取失败则插入同步队列进行等待;
void acquireInterruptibly(int arg):与acquire方法相同,但在同步队列中进行等待的时候可以检测中断;
boolean tryAcquireNanos(int arg, long nanosTimeout):在acquireInterruptibly基础上增加 了超时等待功能,
在超时时间内没有获得同步状态返回false;
boolean release(int arg):释放同步状态,该方法会唤醒在同步队列中的下一个节点
共享式:
void acquireShared(int arg);共享式获取同步状态,如果获取失败则插入同步队列进行等待;
void acquireSharedInterruptibly(int arg);可响应中断
boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
boolean releaseShared(int arg)
独占式锁获取锁的过程是互斥的,就是排队阻塞,共享式就是允许多个线程同时获取锁。
独占锁获取过程
使用ReentrantLock.lock()方式获取锁,以debug方式查看,实际上是调用AQS的acquire() 方法。获取锁失败就会进入同步队列。
//独占式获取锁,如果获取同步状态成功直接返回。如果获取同步状态失败
①加入同步队列自旋获取同步状态
②自旋过程中可响应中断,中断则跳出自旋。注意中断不会阻止线程继续执行。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1、如果同步状态获取成功(得到锁),返回true,执行线程。
2、获取锁失败调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法。addWaiter方法会将线程封装成一个node节点使用尾插法的方式插入同步队列,addWaiter方法内部调用enq方法实现①尾结点为空时会初始化头结点和尾结点②cas操作失败会不断尝试使用尾插法将当前节点插入同步队列
tryAcquire(int arg)方法:
//AQS提供的模板方法 arg = 1
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//NonfairSync重写 acquires = 1
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取同步状态 默认为0
int c = getState();
//默认值,也就是当前锁未被任何线程占有,当前线程首次尝试获取锁
if (c == 0) {
//这是一个cas操作改变同步状态state old值 0 新值 acquires=1
if (compareAndSetState(0, acquires)) {
//如果cas成功当前线程为同步状态的拥有者
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前线程已经获取过该锁,也就是重入
else if (current == getExclusiveOwnerThread()) {
//支持重入加一
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//更新同步状态
setState(nextc);
return true;
}
//同步状态已经被获取(锁已经被占有)且不是重入操作,返回false
return false;
}
小结:这个方法分三种情况:
①如果同步状态是首次被获取,通过cas操作来改变同步状态的值。cas成功:设置当前线程为同步状态的拥有者,返回true 。 cas失败:已经有其他线程修改了同步状态,返回false,准备加入同步队列。
②重入式的获取同步状态,同步状态加一,返回true。
③返回false准备加入同步队列操作
=================接下来是线程加入同步队列相关操作================
addWaiter()& enq()方法
此方法在获取同步状态失败才会调用
// mode = Node.EXCLUSIVE = null
private Node addWaiter(Node mode) {
//构建一个新节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//引用拷贝,获取同步队列尾结点引用
Node pred = tail;
if (pred != null) {
//尾结点不为空则 尾插法插入当前节点
node.prev = pred;
//cas操作将当前节点设置为尾结点
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
//CAS操作,初始化头结点 头结点尾结点指向同一个node
if (compareAndSetHead(new Node()))
tail = head;
} else {
//尾插法插入同步队列。设置当前节点的前驱结点为tail(尾结点)
node.prev = t;
//cas设置当前节点为 尾结点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
addwaiter大致分三步:
①将线程包装成node
②如果尾结点(tail)不为空,说明同步队列已经初始化过了,那么使用尾插法插入当前节点
③如果尾结点为空,则调用enq方法,初始化头结点和尾结点,然后以尾插法插入当前节点
=================接下来是线程自旋尝试获取锁相关操作================
acquireQueued方法和shouldParkAfterFailedAcquire方法
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)) {
//当前节点设置为头结点,Thread和prev设为null 切断前驱节点联系
setHead(node);
//切断后驱节点联系
p.next = null; // help GC
failed = false;
//返回false直接获取锁
return interrupted;
}
//如果前驱节点等待状态不是SIGNAL(-1) 返回false 自旋直到前驱节点的状态为signal
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//对于独占锁,不会响应中断异常
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
这里会有一个疑问关于finally代码块的执行到底在return前还是return后。 cancelAcquire(node);是一个取消节点获取锁的方法,会将对应节点从同步队列中移除。
①当前节点前驱节点为头结点并可以获取同步状态
②cas操作修改当前节点的前驱节点状态为-1 并 用LockSuport.park()方法阻塞当前节点的线程
如果当前节点的前驱节点为头结点并可以获取同步状态,则会进行出队操作,出队逻辑是:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
//断的后驱节点联系
p.next = null; // help GC
failed = false;
//返回false直接获取锁
return interrupted;
此刻同步队列会发生变化:
原:
后:
shouldParkAfterFailedAcquire方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱节点等待状态
int ws = pred.waitStatus;
//如果为-1直接返回
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.
*/
//找到等待状态小于0的前驱节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//断开中间等待状态大于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.
*/
//否则 等于0 小于-1(PROPAGATE-3) cas设置前驱结点等待状态为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
①当前节点前驱节点状态为-1时,返回true
②当前节点前驱节点大于0(CANCELLED)也就是线程被取消。向前遍历找到节点状态<=0的节点,过滤中间节点状态大于0的线程。
③CAS修改前驱节点的状态为-1
小结:
acquireQueued方法是一个自旋逻辑,如果当前节点的前驱节点为头节点(head)并且可以获取同步状态,那么会执行出队操作并跳出自旋,获取同步状态(得到锁)。否则会阻塞等待其他线程释放锁。shouldParkAfterFailedAcquire(p, node) 方法:①剔除当前节点的前驱节点等待状态(wautStatus)大于0(CANCELLED 1)的节点 ②不断尝试将前驱节点状态修改为-1③使用lockSuport阻塞线程
独占锁释放过程
release&tryRelease方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//会调用lockSuport.unpark释放后继节点状态为-1的线程
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;
//同步状态释放成功,移除同步状态拥有者,设置同步状态为0
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
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;
//等待状态小于0,设置等待状态为0
if (ws < 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;
//如果当前节点后驱节点为空或状态大于0(即被中断)时从尾节点开始遍历得到当前节点最近后驱节点
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;
}
//当前节点不为空,且状态小于0 唤醒该节点包装线程
if (s != null)
LockSupport.unpark(s.thread);
}
首先获取头节点的后继节点,当后继节点不为空的时候会调用LookSupport.unpark()方法,该方法会唤醒该节点的后继节点所包装的线程。因此,每一次锁释放后就会唤醒队列中该节点的后继节点所引用的线程,从而进一步可以佐证获得锁的过程是一个FIFO(先进先出)的过程。
独享式锁获取释放总结:
①获取锁成功,即tryAquire返回true
②获取锁失败,即tryAquire返回false
- 线程获取锁失败,线程被封装成Node进行入队操作,核心方法在于addWaiter()和enq(),同时enq()完成对同步队列的头结点初始化工作以及CAS操作失败的重试;
- 线程获取锁是一个自旋的过程,当且仅当 当前节点的前驱节点是头结点并且成功获得同步状态时,节点出队即该节点引用的线程获得锁,否则,当不满足条件时就会调用LookSupport.park()方法使得线程阻塞;
- 释放锁的时候会唤醒后继节点;
总体来说:在获取同步状态时,AQS维护一个同步队列,获取同步状态失败的线程会加入到队列中进行自旋;移除队列(或停止自旋)的条件是前驱节点是头结点并且成功获得了同步状态。在释放同步状态时,同步器会调用unparkSuccessor()方法唤醒后继节点。