并发

Java并发:AbstractQueuedSynchronizer详解(独占模式-C

概述AQS(AbstractQueuedSynchronizer)是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS很容易并且高效地构造出来。不仅ReentrantLock和Semaphore是基于AQS构建的,还包括CountDownLatch、ReentrantReadWriteLock、SynchronousQueue和FutureTask。AQS解决了在实现同步器时涉及的大量细节问

概述

AQS(AbstractQueuedSynchronizer)是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS很容易并且高效地构造出来。不仅ReentrantLock和Semaphore是基于AQS构建的,还包括CountDownLatch、ReentrantReadWriteLock、SynchronousQueue和FutureTask。


AQS解决了在实现同步器时涉及的大量细节问题,例如等待线程采用FIFO队列操作顺序。在不同的同步器中还可以定义一些灵活的标准来判断某个线程是应该通过还是需要等待。基于AQS来构建同步器能带来许多好处。它不仅能极大地减少实现工作,而且也不处理在多个位置上发生的竞争问题(这是在没有使用AQS来构建同步器时的情况)。在 SemaphoreOnLock中,获取许可的操作可能在两个时刻阻塞一一一当锁保护信号量状态时,以及当许可不可用时。在基于AQS构建的同步器中,只可能在一个时刻发生阻塞,从而降低上下文切换的开销,并提高吞吐量。在设计AQS时充分考虑了可伸缩性,因此java.util.concurrent 中所有基于AQS构建的同步器都能获得这个优势。


 


AbstractQueuedSynchronizer介绍

大多数开发者都不会直接使用AQS,标准同步器类的集合能够满足绝大多数情况的需求。但如果能了解标准同步器类的实现方式,那么对于理解它们的工作原理是非常有帮助的。


在基于AQS构建的同步器类中,最基本的操作包括各种形式的获取操作和释放操作。获取操作是一种依赖状态的操作,并且通常会阻塞。当使用锁或信号量时,“获取”操作的含义就很直观,即获取的是锁或者许可,并且调用者可能会一直等待直到同步器类处于可被获取的状态。在使用CountDownLatch时,“获取”操作意味着“等待并直到闭锁到达结束状态”,而在使用FutureTask时,则意味着“等待并直到任务已经完成”。“释放”并不是一个可阻塞的操作,当执行“释放”操作时,所有在请求时被阻塞的线程都会开始执行。


如果一个类想成为状态依赖的类,那么它必须拥有一些状态。AQS负责管理同步器类中的状态,它管理了一个整数状态信息,可以通过getstate,setState以及compareAndSetState等 protected类型方法来进行操作。这个整数可以用于表示任意状态。例如,ReentrantLock用它来表示所有者线程已经重复获取该锁的次数,Semaphore用它来表示剩余的许可数量,FutureTask 用它来表示任务的状态(尚未开始、正在运行、已完成以及已取消)。在同步器类中还可以自行管理一些额外的状态变量,例如,ReentrantLock保存了锁的当前所有者的信息,这样就能区分某个获取操作是重人的还是竞争的。


 


重要入口方法

AQS里面最重要的就是两个操作和一个状态:获取操作(acquire)、释放操作(release)、同步状态(state)。两个操作通过各种条件限制,总共有8个重要的方法,6个获取方法,2个释放方法,如下:


acquire(int):独占模式的获取,忽略中断。

acquireInterruptibly(int):独占模式的获取,可中断

tryAcquireNanos(int, long):独占模式的获取,可中断,并且有超时时间。

release(int):独占模式的释放。

acquireShared(int):共享模式的获取,忽略中断。

acquireSharedInterruptibly(int):共享模式的获取,可中断

tryAcquireSharedNanos(int, long):共享模式的获取,可中断,并且有超时时间。

releaseShared(int):共享模式的释放。

而各个获取方法和释放方法其实大同小异,因此本文只对acquire(int)和release(int)方法展开详解(即独占模式下忽略中断的获取和释放),搞懂了这2个方法,读懂其他6个方法也是基本没有什么阻碍。


 


几个点

一些比较难理解或者容易搞混的知识点,先在这里介绍一下,有助于阅读本文和源码。


注意区分文中提到的队列是“同步队列”还是“条件队列”。“同步队列”通过prev属性和next属性来维护队列,“条件队列”通过nextWaiter属性来维护队列。另外,有些书将prev属性和next属性维护的队列称为“同步队列”,将nextWaiter维护的队列称为“等待队列”。根据源码的注释,其实两个队列都可以称为“等待队列”,因此特以“同步队列”和“条件队列”来区分,请注意。注:本文讲的内容基本都是“同步队列”,“条件队列”是用于Condition的实现。(参考基础属性中的图)

nextWaiter可以分为3种情况:1)共享模式的节点,值固定为源码中的常量SHARED;2)独占模式的普通节点:值固定为源码中的常量EXCLUSIVE,也就是null;3)独占模式的条件队列节点:值指向下一个线程等待在Condition上的节点。如果觉得不好理解,可以参考基础属性下面的图。

AQS里的队列是“CLH”锁定队列的变种, CLH通常用于自旋锁。

prev属性主要用于处理CANCELLED状态。如果节点被取消,其后继节点会向前遍历重新链接到未被取消的前驱节点。

acquire(int) 和 release(int) 方法解释起来比较拗口,正常的语法,动词后面应该带有名词,例如:acquireLock,但是在AQS的源码中并没有这样。因此,在本文中可能会将acquire直接解释成“获取”或直接用“acquire”。

在实际的使用中,acquire一般都指获取锁。如ReentrantLock中的实现。

文中提到的唤醒后继节点,即对后继节点的线程使用LockSupport.unpark方法,与之前的park方法(阻塞节点线程)对应。

head节点(头节点)一般是指当前acquire成功的节点(通常就是当前获取到锁的节点),在设置成头节点后,会将该节点的线程设置为null。

waitStatus=CANCELLED的节点是要丢弃(跳过)的节点,在cancelAcquire(Node)方法中,最直接的办法应该是将node节点移除,但是源码中进行了更优的处理,再移除node节点的同时,将node前面和后面的连续节点waitStatus=CANCELLED的也一并移除了。(参考下文cancelAcquire方法的图)

 


基础属性

static final class Node {
    /** Marker to indicate a node is waiting in shared mode */
    static final Node SHARED = new Node();  // 标记节点正在以共享模式等待
    /** Marker to indicate a node is waiting in exclusive mode */
    static final Node EXCLUSIVE = null; // 标记节点正在以独占模式等待
 
    // 表示线程已取消:由于在同步队列中等待的线程等待超时或者被中断,
    // 需要从同步队列中取消等待,节点进入该状态将不会变化(即要移除/跳过的节点)
    static final int CANCELLED =  1;    
    // 表示后继节点处于park,需要唤醒:后继节点的线程处于park,而当前节点的线
    // 程如果进行释放操作或者被取消,将会通知后继节点,使后继节点的线程得以运行
    static final int SIGNAL    = -1;    
    // 表示线程正在等待状态:即节点在等待队列中,节点线程等待在Condition上,
    // 当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中
    //(即该节点的线程调用了Condition.await()方法,需要先唤醒才能进入同步队列)
    static final int CONDITION = -2;
    // 表示下一次共享模式同步状态获取讲会无条件地被传播下去
    static final int PROPAGATE = -3;
    // 即上面的CANCELLED/SIGNAL/CONDITION/PROPAGATE,初始状态为0
    volatile int waitStatus;    // 等待状态
 
    volatile Node prev; // 前驱节点
 
    volatile Node next; // 后继节点
 
    volatile Thread thread; // 节点的线程(获取同步状态的线程)
    // 条件队列(注意和同步队列区分)中的后继节点:参见addConditionWaiter方法,
    // 表示下一个等待Condition的Node,如果当前节点是共享的,那么这个字段将是一个        
    // SHARED常量,也就是说节点类型(独占和共享)和等待队列中的后继节点共用一个字段。
    Node nextWaiter; 
 
    final boolean isShared() {  // 如果节点在共享模式下等待,则返回true。
        return nextWaiter == SHARED;
    }
    // 返回节点的前驱节点,如果为null,则抛出NullPointerException
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
        

原创不易,完成人机校验,阅读全文

相关推荐