JUC之AQS
- 前言
- CLH队列讲解
- 独占式
- tryAcquire(arg)
- shouldParkAfterFailedAcquire(Node pred, Node node) 如果前面的结点的等待状态为signal,则返回true,如果前面的结点取消等待了,那么就把当前结点,移到最近的活跃的结点,如果前面的结点的等待状态不为signal 也不是取消的,则把前面状态设置为signal,也可能失败,前面的结点刚刚释放完,调用了unparkSuccessor方法
- acquireInterruptibly(int arg)
- tryAcquireNanos(int arg,long nanos)
- release(int arg)
- unparkSuccessor(Node node) 将首节点的状态设置为0,并且唤醒最近的存活的结点
- 共享式
- acquireShared(int arg)
- setHeadAndPropagate(node, r);获取共享锁成功了,就会进入该方法设置首节点,如果还有共享资源,就去唤醒后面的结点,如果首结点已经被释放了,就把首节点设置成为
- doReleaseShared()
- releaseShared(int arg)共享状态的释放
前言
还记得刚学到AQS的时候,每天上完班回到家看书,看帖子差不多看了一个多星期才差不多看完。当时就是各种不理解,记不住。比如说。。。 今天再一次开篇帖子讲AQS,算是温故而知新了。刚开始我们学synchronized,但是synchronized 少了很多锁的特性,比如说:取锁与释放锁的可操作性,可中断、超时获取锁,而且进入到重量级锁的时候,也会效率很慢。后面就学到了Lock锁.里面有一个组件,很多锁都用到了该组件。那个组件就是AQS
AbstractQueuedSynchronizer 是一个抽象类,其实现里面有FairSync类,NonFairSync等类,继承了AbstractOwnableSynchronizer这个类,AbstractOwnableSynchronizer这个类里面并没有太多的方法,所以主要还是得看AbstractQueuedSynchronizer里面是怎么进行构建的。毕竟aqs是众多juc组件的核心
AQS解决了实现同步器时涉及当的大量细节问题,例如获取同步状态、FIFO同步队列。基于AQS来构建同步器可以带来很多好处,它不仅能够极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。
在基于AQS构建的同步器中,只能在一个时刻发生阻塞,从而降低上下文切换的开销,提高了吞吐量。同时在设计AQS时充分考虑了可伸缩性,因此J.U.C中所有基于AQS构建的同步器均可以获得这个优势。
AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。
AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state = 0时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作,当然AQS可以确保对state的操作是安全的。
AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。
AQS主要提供了如下一些方法:
-
getState():返回同步状态的当前值;
-
setState(int newState):设置当前同步状态;
-
compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性;
-
tryAcquire(int arg):独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态;
-
tryRelease(int arg):独占式释放同步状态;
-
tryAcquireShared(int arg):共享式获取同步状态,返回值大于等于0则表示获取成功,否则获取失败;
-
tryReleaseShared(int arg):共享式释放同步状态;
-
isHeldExclusively():当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占;
-
acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法;
-
acquireInterruptibly(int arg):与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException异常并返回;
-
tryAcquireNanos(int arg,long nanos):超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true;
-
acquireShared(int arg):共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;
-
acquireSharedInterruptibly(int arg):共享式获取同步状态,响应中断;
-
tryAcquireSharedNanos(int arg, long nanosTimeout):共享式获取同步状态,增加超时限制;
-
release(int arg):独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒;
-
releaseShared(int arg):共享式释放同步状态;
当我们点开AQS的类进行查看的时候,我们可以看到Doug lea 大佬 写到了一些注释
The wait queue is a variant of a “CLH” (Craig, Landin, and * Hagersten) lock queue. CLH locks are normally used for * spinlocks.
等待队列是“CLH”(Craig、Landin和* Hagersten)锁队列的变体。CLH锁通常用于*自旋锁。
他说等待队列,是CLH锁队列的变体,借此我强烈推荐各位去看看CLH队列的讲解 再去看AQS会开朗很多的
CLH队列讲解
AQS是一个模板类,提供了大量的模版方法,供实现类去实现,比如说
acquire方法,等
可以看出AQS主要分为三类,第一类:独占式的获取和释放同步状态,第二类:共享式的获取
先看到模版方法
独占式
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(arg)
第一个方法 tryAcquire(arg)方法,尝试去获取锁,如果获取失败了,就走acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法
tryAcquire(arg)addWaiter(Node.EXCLUSIVE) 如果获取锁失败,将节点加入到CLH队列的尾部
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讲node节点设置为尾部节点
if (compareAndSetTail(pred, node)) {
// 成功了,将以前的尾部节点的next引用指向尾部节点
pred.next = node;
return node;
}
}
enq(node);
return node;
}
如果尾部节点为null,或者是cas设置尾部节点失败了,自旋的设置添加到尾部队列
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 如果为空,cas初始化一个node
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued:当前线程会根据公平性原则来进行阻塞等待(自旋),直到获取锁为止;并且返回当前线程在等待过程中有没有中断过。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 此方法主要用于检查状态,看看自己是否真的可以去休息了,以免队列前边的线程都放弃了,自己进行盲等
if (shouldParkAfterFailedAcquire(p, node) &&
//如果被中断过,标记已经中断
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire(Node pred, Node node) 如果前面的结点的等待状态为signal,则返回true,如果前面的结点取消等待了,那么就把当前结点,移到最近的活跃的结点,如果前面的结点的等待状态不为signal 也不是取消的,则把前面状态设置为signal,也可能失败,前面的结点刚刚释放完,调用了unparkSuccessor方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 如果前面的节点,是等待被通知的状态,直接返回true
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
// 如果前面的节点已经被取消了,就开始往前面替换节点,直到前一个节点不是取消状态,被替换的节点,形成一个无引用链,被gc回收掉
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;
} 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.
*/
//如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。
18 //有可能失败,前驱说不定刚刚释放完。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
selfInterrupt() 将当前线程中断。
acquire的流程图如下:
acquireInterruptibly(int arg)
独占式响应中断方法,acquire(int arg) 这个方法可以独占式获取锁,但是该方法对中断不响应,对线程进行中断操作后,该线程会依然位于CLH同步队列中等待着获取同步状态,所以aqs提供了一个可以响应中断的方法
// 如果线程被中断,则直接抛出异常,如果获取资源失败,则进入后面的方法
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
这个方法其实和acquire方法中的acquireQueue(addWaiter(Node.EXCLUSIVE), arg)方法差不多,只是它多了对线程中断的异常抛出
doAcquireInterruptibly(arg)
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
tryAcquireNanos(int arg,long nanos)
AQS 除了提供上面的两种方法外,还提供了一个增强版的方法:tryAcquireNanos(int arg,long nanos)。该方法为acquireInterruptibly方法的进一步增强,它除了响应中断外,还有超时控制。即如果当前线程没有在指定时间内获取同步状态,则会返回false,否则返回true。如下:
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
具体的方法看doAcquireNanos(arg, nanosTimeout); 方法
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
// 如果时间已经超时,则返回false
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
// 首先要明白1000纳秒是个非常小的数字,而小于等于1000纳秒的超时等待,无法做到十分的精确,那么就不要使用这么短的一个超时时间去影响超时计算的精确性,所以这时线程不做超时等待,直接做自旋就好了。
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
release(int arg)
独占式同步状态释放,当线程获取到同步状态后,执行完相应的逻辑后就应该去释放同步状态
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
unparkSuccessor(Node node) 将首节点的状态设置为0,并且唤醒最近的存活的结点
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;
// cas状态设置成最初始的状态 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;
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);
}
有关于为什么要从尾部进行遍历,去找到最近的一个结点的线程进行唤醒,可以参考这篇文章
文章参考
共享式
共享式与独占式的最主要区别在于同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。
acquireShared(int arg)
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
//获取失败,自旋获取同步状态
doAcquireShared(arg);
}
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);
// 如果tryAcquireShared的返回值>=0,说明线程获取共享锁成功了,那么调用setHeadAndPropagate,然后函数即将返回,如果小于0,则说明获取共享锁失败,那么就进入shouldParkAfterFailedAcquire(p, node)方法
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);
}
}
setHeadAndPropagate(node, r);
setHeadAndPropagate(node, r);获取共享锁成功了,就会进入该方法设置首节点,如果还有共享资源,就去唤醒后面的结点,如果首结点已经被释放了,就把首节点设置成为
这个方法主要就是
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
//如果还有共享锁可以获取,或者是旧head的status<0,只能是由于doReleaseShared里的compareAndSetWaitStatus(h, 0, Node.PROPAGATE)的操作,而且由于当前执行setHeadAndPropagate的线程只会在最后一句才执行doReleaseShared,所以出现这种情况,一定是因为有另一个线程在调用doReleaseShared才能造成,而这很可能是因为在中间状态时,又有人释放了共享锁。propagate == 0只能代表当时tryAcquireShared后没有共享锁剩余,但之后的时刻很可能又有共享锁释放出来了。unparkSuccessor这个方法会将首节点的状态设置成0
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 这里为什么可以为null,主要是因为enq,里面将结点加入到尾部是,if代码块里面的代码并不是原子性的。主要看unparkSuccessor这个方法的解释。
if (s == null || s.isShared())
doReleaseShared();
}
}
doReleaseShared()
private void doReleaseShared() {
for (;;) {
// 这里拿到的head已经是当前线程的node结点了
Node h = head;
// 如果不为空,或者是不是只有一个结点
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
// 将首部结点的状态设置为0,如果修改成功,成功,则唤醒最近的还存活的结点,如果不是最近的结点的话,重新进入doAcquireShared()方法的话,会进行一个结点的替换,替换到最近的活跃的结点shouldParkAfterFailedAcquire 这个方法
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
// 如果已经有线程释放了共享锁,会进入这个方法
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 防修改的不是首节点,因为这个时候可能有其他的线程进来,抢占了首节点
if (h == head) // loop if head changed
break;
}
}
releaseShared(int arg)共享状态的释放
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
同样的 获取共享资源也有共享式响应中断的方法,和超时获取的方法。