AQS总结
一.介绍
1 . 队列同步器AbstractQueuedSynchronizer(简称同步器), 用来构建锁或者其他同步组件的基础框架,它使用一个int成员变量(用volatile修饰的) 表示同步状态,通过内置的FIFO同步队列来完成对资源获取线程的排队工作
.
2 . 同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,对同步状态state的管理有三个方法:
getState():获取当前同步状态
setState(int newState):设置当前同步状态
compareAndSetState(int expect, int update):使用CAS设置当前状态,保证原子性。
通过这三个方法能保证对同步状态的修改是安全的。
3 .同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解:
1.锁是面向使用者的,它定义了使用者和锁交互的接口;
2.同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理,线程的排队,等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者关注的领域。
双向FIFO等待队列,自旋锁,使用队列结点对象Node包装要获取锁的线程
AQS通过一个状态变量state,来标志当前线程持有锁的状态。
state = 0时代表没有持有锁,> 0 代表持有锁。
当队列中一个结点释放锁时,会唤醒后继阻塞的线程
队列同步器的接口
- 使用者要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
- 同步器提供的模板方法分为3类:独占式获取和释放同步状态,共享式获取和释放同步状态,独占式超时获取同步状态。
具体的接口方法和三种同步器模板方法实现下面介绍。
二.队列同步器的实现分析
1.同步队列介绍
- 同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理。当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个节点(node)并将其加入同步队列,同步时会阻塞当前线程,当同步状态释放时,会将首节点中的线程唤醒,使其再次尝试获取同步状态。
- 同步队列中的节点同来保存获取同步状态失败的线程引用,等待状态以及前驱和后继节点。
- 同步器将获取同步状态失败的线程构造成节点加入同步队列尾部时为了保证线程安全是使用CAS操作的:compareAndSetTail(Node expect,Node update); 将获取同步状态成功的线程设置为首节点没有使用CAS操作,因为只有同时只有一个线程能获取到同步状态。
.
2.独占式同步状态获取和释放
通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断是不敏感的,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出。
这是获取时调用的方法,下面将分析acquire方法中的三个调用方法;
public final void acquire(int arg) {
if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//表示去阻塞当前节点node中的线程
selfInterrupt();
}
1). 分析获取同步状态的acquire获取方法
过程分析:
- 首先是调用自定义同步器实现的tryAcquire(int arg)方法,该方法使用CAS操作保证线程安全的获取同步状态(同步状态设置为1),如果是获取同步状态失败时返回false;
- 然后进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法:在前一个方法获取同步状态失败时线程已经被构造成节点了,这个方法中的addWaiter是使用for(;;)死循环并使用CAS操作将节点设置为尾节点,(Node.EXCLUSIVE表示独占式,即同一时刻只有一个线程能获取到同步状态),失败线程添加到尾节点后使用acquireQueued方法使该节点以for(;;)死循环方法去获取同步状态.
- 方法括号中的内容都符合时,表示该线程没有获取到同步状态,且加入同步队列中去自旋获取同步状态了,然后执行selfInterrupt();方法,表示去阻塞当前节点node中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队列或阻塞线程被中断来实现。
处于同步队列中的线程只有满足它的前驱节点是头节点时才会去尝试获取同步状态,为什么呢?:
1. 头节点是成功获取到同步状态的节点,而头节点线程释放了同步状态后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
2. 维护同步队列的FIFO原则。
3. 防止由于过早通知而被唤醒的线程去竞争同步状态(过早通知是指前驱节点不是头节点的线程由于被中断而被唤醒)。
2)分析释放同步状态release方法
代码如下:
public final boolean release(int arg) {
//调用自己重写的tryRelease方法
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒后继节点
unparkSuccessor(h);
return true;
}
return false;
}
release方法之后后,会唤醒头节点的后继节点线程,unparkSuccessor(Node node)方法使用LockSupport来唤醒处于等待状态的线程。
3)总结
在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程被构造为节点然后加入到队列中进行自旋;移出队列的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。(如果是对于锁而言,代表线程获取与释放锁).
tryAcquire和tryRelease方法是我们继承AbstractQueuedSynchronizer抽象类后重写的方法。
.
3.共享式同步状态获取和释放
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。
为了理解两种不同的获取方式,我们以文件的读写为例:如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。即写操作要求对资源的独占式访问,读操作可以是共享式访问。
1)获取acquireShared方法分析
通过调用同步器的acquireShared(int arg)方法可以共享式地获取同步状态。代码如下
同独占模式一样,共享模式提供了模板方法tryAcquireShared供子类实现
该方法不同于tryAcquire的地方是在于返回值为int类型,而不是boolean
public final void acquireShared(int arg) {
//当tryAcquireShared 返回值小于0时表示未获取到,
//然后调用doAcquireShared 去获取同步状态,当获取到时
//则tryAcquireShared 返回值>=0就退出if循环
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
doAcquireShared(arg)代码如下:
private void doAcquireShared(int arg) {
//Node.SHARED表示:创建共享式的node节点
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);
//判断释放获取到了
if (r >= 0) {
//当前节点获取到了后把当前节点设置为头节点head
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
//被中断了就去阻塞当前节点node中的线程
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
过程分析:在acquireShared方法中,同步器调用tryAcquireShared方法尝试获取同步状态,返回值大于等于0时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquireShared方法返回值大于等于0。可以看到,在doAcquireShared方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示获取到了并退出自旋过程。
2)释放releaseShared方法分析
public final boolean releaseShared(int arg) {
//判断能否释放同步状态
if (tryReleaseShared(arg)) {
//执行释放
doReleaseShared();
return true;
}
return false;
}
它与独占式主要区别在于tryReleaseShared方法必须确保同步状态线程能安全释放,一般通过循环和CAS来保证,因为释放同步状态的操作会来自多个线程。
.
4.独占式超时获取同步状态和释放
这个方法提供了传统synchronized关键字不具有的特性;
通过tryAcquireNanos(int arg, long nanosTimeout)方法在支持响应中断的基础上,增加了超时获取的特性;
tryAcquireNanos超时的获取同步状态,其方法源码如下:
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
从源码中可以看出,首先判断线程是否被中断,若果中断了就抛出InterruptedException异常,否则获取同步状态,如果获取同步状态失败在调用 doAcquireNanos(arg, nanosTimeout)方法 (先直接去获取速度要更快些)。
我们来看下 doAcquireNanos(arg, nanosTimeout)方法的定义:
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (nanosTimeout <= 0 L)
return false;
// 截止时间
final long deadline = System.nanoTime() + nanosTimeout;
// 独占式添加到CHL尾部
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 <= 0 L)
return false;
// 若果没有超时,则等待nanosTimeout纳秒
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
// 判断线程是否被中断
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
过程分析:我们可以看到在doAcquireNanos(int arg, long nanosTimeout)方法中,首先判断超时时间是否小于等于0,如果小于等于0则返回false。若果超时时间大于0则计算出截止时间(final long deadline = System.nanoTime() + nanosTimeout;)若果当前节点不是头结点获取获取同步状态失败,则需要计算出睡眠时间(nanosTimeout = deadline - System.nanoTime();),如果睡眠时间小于等于0,则返回false,否则如果超时时间大于spinForTimeoutThreshold(1000L),则睡眠nanosTimeout纳秒,否则进入自旋。这里spinForTimeoutThreshold是AQS定义的一个常量,这里为什么要定义一个超时阈值呢?这是因为在线程从睡眠(TIME_WAITINT)状态切换到RUNNING状态会导致上下文的切换,如果超时时间太短,会导频繁的上下文切换而浪费资源。
整个超时控制的流程如下:
1)独占式与超时独占式的区别
独占式获取与超时独占式获取的区别:独占式在为获取到同步状态时,会使当前线程一直处于等待状态,而超时独占式会使线程等待nanosTimeout纳秒,如果当前线程在nanosTimeout纳秒内没有获取到同步状态,将会返回。