首页 > 技术文章 > AQS 详解

wuzhiwei549 2018-04-06 15:25 原文

基本

同步器至少要有以下两种类型的方法acquire和release

  • acquire:至少要有一个操作能实现对调用线程的阻塞,直到同步器允许它进行操作。
  • release:至少要有一个操作能用一种方式解锁一个或者更多个已经阻塞的线程改变同步状态。

同时,同步器还需要支持以下几种功能:

  • 非阻塞式的同步过程尝试(tryLock)
  • 可选的超时机制,可以允许程序放弃等待
  • 可以通过中断执行取消

而为了适应不同的同步器,同步器要支持两种模式

  • 独占式 exclusive。要保证一次只有一个线程可以经过阻塞点
  • 共享式 shared。可以允许多个线程阻塞点

1.结构定义

AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer)用于构建锁和同步容器的抽象类,定义了同步状态的获取和释放的方法来供自定义的同步组件的使用,子类通过组合的方式使用。
AQS继承了AbstractOwnableSynchronizer,这个类只有一个变量:exclusiveOwnerThread,表示当前占用该锁的线程,并且提供了相应的get,set方法。
  /**
     * The current owner of exclusive mode synchronization.
     */
    private transient Thread exclusiveOwnerThread;
AQS的实现依赖内部的同步先进先出队列(FIFO双向队列)表示排队等待锁的线程。队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus。如果当前线程获取同步状态失败,AQS会将该线程以及等待状态等信息构造成一个Node,将其加入同步队列的尾部,同时阻塞当前线程,当同步状态释放时,唤醒队列的头节点。
AQS核心成员变量
  /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;

    /**
     * The synchronization state.
     */
    private volatile int state;
提供方法
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):共享式释放同步状态;

2.实现

2.1 状态
int state (4个字节)的变量来持有同步状态。使用getState, setState, compareAndSetState方法来进行状态的获取和更新。 对state变量值的更新都采用CAS操作保证更新操作的原子性。例如ReentrantLocky用它表示线程重入锁的次数,Semaphore用它表示剩余的许可数量,FutureTask用它表示任务的状态。 它的tryAcquire和tryRelease方法都需要子类去实现。两个方法都支持传入一个int类型的参数。这个参数主要用来实现不同子类功能的。例如:reentrant lock,当在返回一个条件等待后重新去获取锁权限是,它会重新建立一个递归计数。

2.2 阻塞

AQS没有采用Thread.suspendThread.resume这两种方式,以上两种方式都有严重的安全问题,容易造成死锁等。AQS采用了java.util.concurrent.locks包下的LockSupport类。该类可以响应中断操作,可以设置超时时间等。

2.3 获取同步状态
初始状态下(state=0),当线程A顺利获取锁(state=1)在持有锁期间,线程B再进行获取时,(state=1)锁被占用,将线程信息和等待状态等信息构成出一个Node节点对象(pred.next = node),放入同步队列,head和tail分别指向队列的头部和尾部(如第一张图,此时队列中有一个空的Node节点作为头点,head指向这个空节点,空Node的后继节点是B对应的Node节点,tail指向它),同时阻塞线程B(这里的阻塞使用的是LockSupport.park()方法,在调用park方法前,线程会设置一个“SIGNAL”信号,然后重新检查同步状态,再确定是否需要再次调用park方法)。后续如果再有线程要获取锁,都会加入队列尾部并阻塞。
    AQS需要控制在头节点调用tryAcquire方法适合才允许通过,其他情况acquire和block都会失败。每次只需检查当前节点的前驱节点是不是head,这一点减少了CLH对内存的读取竞争,同时还能避免不必要的阻塞和唤醒操作。
2.4 释放同步状态
当线程A释放锁时(state=0),此时A会唤醒头节点的后继节点(所谓唤醒,其实是调用LockSupport.unpark(B)方法),即B线程从LockSupport.park()方法返回,此时B发现state已经为0,所以B线程可以顺利获取锁,B获取锁后B的Node节点随之出队。它有两个原子操作更新域,head和taiil。初始化时,将指向一个虚假的节点。 
每个节点的release状态都保存在它的前驱节点内,while (pred.status != RELEASED);后就可以开始自旋。若持有前驱节点的域,CLH锁可以处理超时和其他形式的取消操作。

3.条件队列

AQS中的ConditionObject提供一个能让同步器使用的类。它既符合Lock接口,又能持有互斥的同步机制。
ConditionObject类提供了类似await,signal,signalAll操作的API。这些方法的作用与Object.wait方法是一样的。ConditionObject使用与同步器同样的内部队列,不过与同步器存储在分开的条件队列中。

3.1 基本的await操作如下: 
1. 创建并添加新的节点到条件队列中; 
2. 释放锁; 
3. 阻塞直到节点在锁队列中 
4. 重新获取锁。

3.2 基本的signal操作如下: 
1. 传递条件队列的第一个节点到锁队列中

3.3 处理取消,超时和线程的中断

- 中断发生在await操作之前,此方法一定要抛出一个InterruptedException 
- 中断发生在await操作之后,此方法不抛出异常,而是系统的中断状态集

条件队列需要一个状态位,当出现Signal信号失败,就将信号传递到队列的下一个节点内。而如果出现Cancel信号失败,就取消传递操作,唤醒锁的重新获取操作。

4.使用

4.1 公平性

AQS并不保证同步器一定是公平的。tryAcquire方法是在入队操作前的一个检验,因此完全可以在入队前,“偷取”获取的权限。

  • 非公平的FIFO策略(获取到锁的顺序不一定是队列中的顺序),将tryAcquire方法中每次进入都会进行竞争,无论当前线程是否是队列的头节点。只要进入的线程速度更快,那么队列中的节点即使解除了阻塞,依然会重新阻塞回去。

  • 公平的FIFO策略,只需要将tryAcquire方法在当前线程不是队列的头节点时放回失败就行。次之的方式,只需要判断队列是否为空,空队列就可以放回tryAcquire成功。

同步器的公平性设置主要是在多处理器情况下,才能发挥出其水平。多处理器往往会有更多的竞争,也就更有可能发生一个线程发现锁现在被其他线程需要的情形。

4.2 同步器

ReentrantLock
ReentrantReadWriteLock
Semaphore
CountDownLatch
FutureTask(1.5以后不再使用AQS)
SynchronousQueue

参考文章:
  1. Doug Lea. The java.util.concurrent Synchronizer Framework
  2. https://blog.csdn.net/teen11/article/details/69371876?locationNum=16&fps=1

推荐阅读