一.简介
AQS即AbstractQueuedSynchronizer,队列同步器,很多并发工具都使用它作为基础框架,像
锁(ReenTrantLock、ReentTrantReadWriteLock),工具类(CountDownLatch、CyclicBarrier、
Semaphore)都是依赖它来完成。
二.CLH队列同步器:
1.AQS内部维护一个FIFO的队列,这个队列就是CLH,通过CLH来完成同步状态的管理。这个
CLH是一个双向队列,是通过双向链表实现,也是一个变种的同步队列,原生的是让线程自旋,
而CLH是让线程睡眠(通过调用底层的park()方法来让线程睡眠并且释放cpu资源)。
2.CLH其实是通过一个双向的Node来实现的,通过对Node类型的区别,来达到共享锁和独占锁
的实现。
通过Node我们可以看到里面维护了两种Node方式,分别是独占和共享,所以CLH队列是可以为
独显锁或者共享锁的线程创建node对象
//共享节点
static final Node SHARED = new Node();
//独占节点
static final Node EXCLUSIVE = null;
//waitStatus的四种状态,其实还有一种,就是0,表示初始化状态
//因为超时或者中断,节点会被设置为取消状态,被取消的节点时不会参与到竞争中的,他会一直保持取消状态不会转变为其他状态
static final int CANCELLED = 1;
//后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行
static final int SIGNAL = -1;
//节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()后,该节点将会从等待队列中转移到同步队列中,加入到同步状态的获取中
static final int CONDITION = -2;
//表示下一次共享式同步状态获取,将会无条件地传播下去
static final int PROPAGATE = -3;
//表示当前NOde的状态
volatile int waitStatus;
//当前node的上一个node
volatile Node prev;
//当前node的下一个node
volatile Node next;
//当前线程
volatile Thread thread;
//下一个node
Node nextWaiter;
这个方法是获取node的上一个节点
//获取当前node的上一个node节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
往队列中添加Node,如果当前队列为空,则通过调用enq()创建空Node作为head,此Node的pre为空,thread也为空,next指向添加进来的Node,新Node的pre指向这个空Node,且tail指向这个新添加进来的Node
/**
* 往队列中添加新节点
* @param mode
* @return
*/
private Node addWaiter(Node mode) {
//创建节点,mode表示节点的类型,独占还是共享
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
//队列已有节点,将新节点添加进队列
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//创建一个空节点,然后在在新节点放在空节点后面
enq(node);
return node;
}
/**
* 当队列为空时,调用此方法
* @param node
* @return
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//创建初始的空节点
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
//tail和head分别指向空节点
tail = head;
} else {
//空节点的next指向新增节点,新增节点的pre指向
node.prev = t;
//设置tail以及空节点的next指向新增节点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
//判断下一个节点是独占节点还是共享节点
final boolean isShared() {
return nextWaiter == SHARED;
}
三.AQS的独占锁
AQS的核心就是CLH同步队列器,通过刚才对CLH源码的分析可以看出AQS是支持独占锁以及共
享锁,对于独占锁来说,以ReentrantLock为例。
使用lock.lock()方法进行加锁时,会调用以下方法,在非公平锁中会通过compareAndSetState(0,
1)方法进行尝试加锁,如果加锁成功,则直接设置状态线程(ExclusiveOwnerThread)为当前线
程;否则加锁成功则直接执行acquire(1)方法,而公平锁是直接执行
//非公平锁调用
final void lock() {
//加锁设置
if (compareAndSetState(0, 1))
//加锁成功设置占有线程为自己
setExclusiveOwnerThread(Thread.currentThread());
else
//加锁失败
acquire(1);
}
//公平锁调用
final void lock() {
acquire(1);
}
在acquire(1)方法中,会进行加锁或者进入CLH操作
/**
* 获取同步锁
* @param arg
*/
public final void acquire(int arg) {
//第一个方法是判断能否获取同步成功,如果成功则返回true
//则后面的就不走了。如果返回false,则走第二方法讲线程添加进CLH中
//第三个方法是线程中断
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
在上面的调用中,最大会调用三个方法,我把它称为加锁三部曲。首先一部曲是调用
tryAcquire(arg)方法,这个方法在公平锁和非公平也是不同的操作,在公平锁中是判断CLH队列
是否还有节点,如果没有则进行加锁操作,如果加锁成功则返回true,否则则返回失败,同时这
里也是支持可重入锁操作。而且非公平锁中则是调用nonfairTryAcquire(int acquires)方法,该方
法跟公平锁走的逻辑一样,只是少了一步判断CLH的操作,这个也是公平锁跟非公平锁的区别,
公平锁是如果队列中还有节点,就不去抢占锁而去进入队列中排队,而非公平锁则事不管队列中
有没有,就直接去抢占锁,抢占不到则去队列中排队。
/**
* 公平锁- 尝试去加锁
* @param acquires
* @return
*/
protected final boolean tryAcquire(int acquires) {
//获取当前线程以及同步状态
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//当没有线程占用的时候,判断是否是空队列,如果是空队列设置值,在非公平锁中没有判断队列这一步
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;
}
}
//为空则返回false
public final boolean hasQueuedPredecessors() {
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());
}
/**
* 非公平锁tryAcquires调用
* @param acquires
* @return
*/
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程以及state
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
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;
}
return false;
}
当一部曲tryAcquire(arg)返回true时,则表明加锁成功,因为前面有个!,则表明结果是false,后面的二三部曲讲不会执行,当加锁失败返回false是,则结果是true,则会执行二部曲acquireQueued(addWaiter(Node.EXCLUSIVE), arg),而addWaiter()方法刚才讲过,是一个往CLH队列中添加节点的操作,后面Node.EXCLUSIVE表明我们添加的是独占锁的节点。当我们添加节点成功之后,会返回该节点对象作为acquireQueued()方法的参数。而在acquireQueued()方法中,我们会有一个自旋的操作。我们会判断当前节点的pre节点是不是头节点,以及尝试去加锁,如果都成功,则讲该节点设置为head节点并且清空节点的线程信息以及pre指引。并且将原head节点指向null方便被回收。如果操作成功则放回当前线程中断状态-false。如果失败则进行下一步的睡眠操作。
/**
* 将添加进队列的线程,通过调用底层的park方法进行睡眠
* @param node
* @param arg
* @return
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//节点自旋的去尝试获取锁
for (;;) {
final Node p = node.predecessor();
//如果该节点的pre是head,将其pre节点的next指向null,方便被GC回收
//另外将自己节点的pre和thread置为空,作为一个空节点,指向head
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//加锁失败,说明pre不是head,将线程进行中断睡眠
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
/**
* 设置当前节点为头节点并且清空相关信息
* @param node
*/
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
睡眠操作中,会首先通过shouldParkAfterFailedAcquire(Node pred, Node node)判断该节点的上一个的waitStatus值。addWaiter()构造的新节点,waitStatus的默认值是0。此时,进入最后一个if判断,CAS设置pred.waitStatus为SIGNAL==-1。最后返回false。回到第五步acquireQueued()中后,由于shouldParkAfterFailedAcquire()返回false,会继续进行循环。假设node的前继节点pred仍然不是头结点或锁获取失败,则会再次进入shouldParkAfterFailedAcquire()。上一轮循环中,已经将pred.waitStatus设置为SIGNAL==-1,则这次会进入第一个判断条件,直接返回true,表示应该阻塞。因为创建的节点的waitstatus都是0,所以都会通过设置来讲waitstatus改变成-1.
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//prev节点通知下一个节点阻塞等待,表示当前node节点可以安全的被park
return true;
if (ws > 0) {
//删除prev节点,不停的调用上一个节点给prev赋值,直到prev的状态大于-
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 0 或者 Node.PROPAGATE
//通过cas设置,讲状态改成Node.SIGNAL 状态
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 返回false,则接着循环执行上一层的for循环
return false;
}
当返回true表示应该阻塞时,则会调用parkAndCheckInterrupt()方法将线程进行睡眠。Thread.interrupted();操作是避免当前线程被其他线程中断了,所以通过这个来唤醒。但是。在这里,当对线程park()之后,线程就不会在执行了,因为线程被睡眠了。所以后面的selfInterrupt()方法,以及返回,还有自旋都不会在执行了,直到当前线程被唤醒。
/**
* 线程睡眠已经线程中断
* @return
*/
private final boolean parkAndCheckInterrupt() {
//调用os底层方法对线程进行睡眠
LockSupport.park(this);
//判断当前线程有没有被其他线程中断操作过,如果有就返回标志并且清除。
return Thread.interrupted();
}
第三部曲就是设置当前线程中断。因为线程被睡眠了。返回了true,所以进行线程中断操作。
static void selfInterrupt() {
//如果当前线程被其他线程中断过,因为park后请出了中断,所以要再次中断。
Thread.currentThread().interrupt();
}
加锁成功后,后执行业务逻辑操作,当释放锁时,会调用lock.unlock()方法。这时候会调用release(int arg)方法进行释放锁。这时候则会首先通过tryRelease(int releases)来改变state的值,也就是减1,然后在释放占有线程。并且将head节点指向null便于GC回收,然后清空当前节点的thread和pre信息,设置waitsratus=0,并将自己设置为head节点。最后通过LockSupport.unpark(s.thread)唤醒下一个线程
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);
}
setState(c);
return free;
}
/**
* 唤醒下一个节点
* @param node
*/
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (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);
}
拿别人的一张图来展示CLH队列关系