抽象队列同步器(AQS)
抽象队列同步器AbstractQueuedSynchronizer,是用来构建同步组件的基础框架。它内部有一个int成员变量表示同步状态,通过FIFO队列来完成获取资源的线程的排队工作。
同步器的使用方式主要是通过继承。子类通过继承同步器并实现它的抽象方法来管理同步状态。AQS提供了对同步状态修改的同步方法。子类一般作为同步器的静态内部类来使用,那么只要自定义实现子类对同步状态的获取方式就可以实现不同种类的同步器。
AQS提供的接口
同步器的设计采用了模板方法模式。子类只需要实现对同步状态获取的管理的抽象方法即可。然后同步器调用AQS提供的模板方法就可以实现不同的同步器功能。
修改同步状态方法
- getState()
- setState(int newState)
- compareAndSetState(int expect, int update)
AQS抽象方法
- tryAcquire(int arg):独占式获取同步状态
- tryRelease(int arg):独占式释放同步状态
- tryAcquireShared(int arg):共享式获取同步状态
- tryReleaseShared(int arg):共享式释放同步状态
- isHeldExclusively():判断当前同步器是否在独占模式下被线程占用
AQS模板方法
- void acquire(int arg):独占式获取同步状态
- void acquireShared(int arg):共享式获取同步状态
- boolean release(int arg):独占式释放同步状态
- boolean releaseShared(int arg):共享式释放同步状态
- Collection getQueuedThreads():获取等待在同步队列上的线程集合
AQS实现分析
同步队列
同步器内部有一个FIFO队列对获取同步状态失败的线程就行管理。当一个线程获取同步状态失败时,同步器会将当前线程和等待状态等信息封装为一个节点加入到同步队列中,同时会阻塞线程。当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
Node节点的属性:
属性类型和名称 | 描述 |
---|---|
int waitStatus | 等待状态(CANCELLED,SIGNAL,CONDITION,PROPAGATE,INITIAL) |
Node prev | 前驱节点 |
Node next | 后继节点 |
Node nextWaiter | 等待队列中的后继节点 |
Thread thread | 该节点对应的线程 |
同步器拥有头节点和尾节点。获取同步状态失败的线程会加入到同步队列的尾部。AQS提供了compareAndSetTail(Node expect, Node update) CAS同步方法设置尾节点。
同步队列遵循FIFO,头节点是获取同步状态的节点,头节点在释放同步状态后会唤醒后继节点。而后继节点将会在获取同步状态成功时将自己设置为头节点。
设置头节点的方法不需要使用CAS,因为只有在线程获取到同步状态后才能设置为头节点。
独占式同步状态获取与释放
同步组件通过调用同步器的acquire(int arg)方法可以获取同步状态。该方法对中断不敏感,获取同步状态的线程失败后加入到同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除。
acquire(int arg)方法源码:
public final void acquire(int arg){
if(!tryAcquire(arg) & acquireQueued( addWaiter(Node.EXCLUSIVE),arg ))
selfInterrupt();
}
acquire方法主要调用了三个方法:tryAcquire(), acquireQueued(), addWaiter();
- tryAcquire():是自定义的获取同步状态的方式。
- addWaiter():创建节点并加入到同步队列的尾部
- acquireQueued():让加入的节点以“死循环”的方式获取同步状态。如果获取不到就阻塞线程。前驱节点出队则会唤醒该节点。
addWaiter(Node mode)方法源码:
private Node addWaiter(Node mode){
Node node =new Node(Thread.currentThread(), mode);
// 先尝试一次添加到尾节点,如果失败,则进入“死循环”进行添加到尾节点。
Node pred = tail;
if(pre != null){
node.prev=pred;
if(compareAndSetTail(pred,node)){
pred.next=node;
return node;
}
}
enq(node); //第一次尝试添加到尾节点失败,进入死循环添加
return node;
}
private Node enq(final Node node) {
for (;;){
Node t = tail;
if(t == null){
if(compareAndSetHead(new Node())) tail =head;
}else{
node.prev=t;
if(compareAndSetTail(t, node)){
t.next=node;
return node;
}
}
}
}
acquireQueued方法源码:
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)) {
setHead(node); //获取同步状态成功后,设置为头节点
p.next = null; //目的是让GC回收它
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; //阻塞当前线程。
}
}finally {
if(failed) cancelAcquire(node);
}
}
只有线程的前驱节点为头节点时才能获取同步状态。如果线程前提被唤醒了(可能发生中断被唤醒),则会检查前驱节点是否为头节点,防止错误获取同步状态。同步队列保证了FIFO。
如线程获取同步状态后,执行完逻辑释放同步状态,则会把它后继节点唤醒,然后后继节点会重新尝试获取同步状态。
release方法源码:
public final boolean release(int arg){
if(tryRelease(arg)){
Node h =head;
if(h!=null&&h.waitStatus!=0){
unparkSuccessor(h); //唤醒后继节点
}
return true;
}
return false;
}
总结:在获取同步状态时,同步器会维护一个同步队列。获取同步状态失败的线程会封装成节点加入到队尾中。头节点为获取同步状态的节点,当头节点释放同步状态时会唤醒后继节点,然后后继节点重新尝试获取同步状态,获取成功后,将自己设置为头节点。
共享式同步状态获取与释放
共享式和独占式获取最主要的区别在于同一时刻是否可以有多个线程同时获取同步状态。
共享式访问资源时,可以多个共享式访问的线程访问资源。独占式访问资源时,只允许一个线程访问资源。
调用acquiredShared(int arg)方法可以共享式地获取同步状态。
acquired方法源码:
public final void acquireShared(int arg){
if(tryAcquireShared(arg) < 0) doAcquireShared(arg); //尝试获取同步状态,如果获取失败则加入同步队列
}
doAcquireShared方法源码:
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); //获取同步状态,若返回值大于零,则同步状态获取成功
if(r>=0) {
setHeadAndPropagate(node, r);
p.next=null;
if (interrupted) selfInterrupt();
failed=false;
return;
}
}
if(shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; //获取同步状态失败,阻塞线程
}
}finally {
if(failed) cancelAcquire(node);
}
}
acquireShared(int arg)模板方法中调用tryAcquireShared(int arg)方法尝试获取同步状态。和独占式运行过程差不多,获取同步状态,获取失败加入到队尾中,头节点已经获取了同步状态,释放后唤醒后继节点自旋重新获取同步状态。只不过在共享模式下同步状态允许多个线程同时获取。
释放同步状态源码:
public final boolean releaseShared(int arg){
if(tryReleaseShared(arg)) {
doReleaseShared(); //唤醒后继节点
return true;
}
return false;
}
在重写tryReleaseShared方法和tryAcquireShared方法时要注意保证线程安全,可能多个线程同时操作,一般用CAS来保证线程安全。
独占式超时获取同步状态
调用同步器的doAcquireNanos(int arg,long nanosTimeout) 方法可以超时获取同步状态,该方法对中断敏感的,如果在等待获取同步状态时,发生中断,那么会立即返回,并抛出异常InterruptedException。
针对超时操作,同步器会计算出要睡眠的时间,nanosTimeout -= now - lastTime,如果nanosTimeout大于0则表示超时还未到,则需要继续睡眠nanosTimeout纳秒,反之表示超时。
doAcquireNanos源码:
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException{
long lastTime = System.nanoTime();
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;
failed=false;
return true;
}
if(nanosTimeout<=0) return false;
if(shouldParkAfterFailedAcquire(p, node) && nanosTimeout>spinForTimeotThreshold) LockSupport.parkNanos(this,nanosTimeout);
long now=System.nanoTime();
nanosTimeout -= now-lastTime;
lastTime=now;
if(Thread.interrupted()) throw new InterruptedException();
}
} finally {
if(failed) cancelAcquire(node);
}
}
这个和独占式获取同步状态类似,但在获取同步状态失败后处理上有些不同。它在同步状态获取失败后,会先判断是否超时,如果已经超时则返回false,如果没有超时则阻塞。唤醒后会重新计算超时时间,并再次尝试获取同步状态。在计算完同步状态后会对线程中断进行判断,从而让该方法响应中断。
如果nanosTimeout小于等于spinForTimeoutThreshold(1000纳秒)时,将不会睡眠该线程,会让线程快速的自旋。时间很短的话,没法做到很精确,而且超时时间很短,则直接快速自旋。
参考
- 《Java并发编程的艺术》