J.U.C包核心AQS(一):快速了解AQS

AQS 是个啥?

AQS,全称 AbstractQueuedSynchronizer,中文直译过来就是 “抽象的队列式的同步器”,它定义了多线程访问共享资源的同步器框架,J.U.C 包中很多同步类实现都依赖于它,比如我们常用的锁:ReentrantLock,又比如一些常用的并发工具类:CountDownLatch、Semaphore 等等。所以,很多人称 AQS 为 J.U.C 包的核心。那么要想吃透 J.U.C 的源码思想,第一步要做的当然就是彻底弄懂 AQS 啊!
 

AQS 的组成

AQS 整体上来看有两个核心组成部分:

  • 一个 int 型的成员变量,它用来表示同步状态。
  • 一个 FIFO 双向队列,它用来完成竞争资源的线程的排队工作。

我们接下来具体的来看一下这两个重要的组成部分。

一、同步状态

private volatile int state;

我们可以看到,成员变量 state 是被关键字 volatile 修饰了的,这里主要是为了保证 state 这个变量在多个线程之间的可见性。

那么所谓的加锁/解锁,其实就是对 state 这个同步状态的获取,类似于操作系统中实现进程同步的信号量,多个进程通过检查信号量的值来判断是否可以进入临界区。

同步器的主要使用方式是继承,子类通过继承 AQS 并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的三个方法来操作同步状态:

  • getState():获取当前同步状态
  • setState(int newState):设置当前同步状态
  • compareAndSetState(int expect,int update):使用 CAS 设置当前状态,该方法能够保证状态设置的原子性

源码如下:

    protected final int getState() {
        return state;
    }

    protected final void setState(int newState) {
        state = newState;
    }
    
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

我们主要来看一下 compareAndSetState(int expect, int update) 这个方法,看名字你就能知道,只要以 ”CompareAndxxxxx “ 开头的,基本上就是使用 unsafe 类的 CAS 操作没跑了,主要是为了保证设置同步状态的操作原子性。

二、同步队列

AQS 通过内置的 FIFO 同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS 则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。

本文旨在先对 AQS 做一个简单的介绍,同步队列的具体内容在该系列的下一篇文章中。

 

AQS 的使用详解与示例

同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

那么,接下来我们就来看看,哪些方法是可以重写的,哪些方法是模板方法。

一、可重写方法:

在介绍可重写方法之前,要先让大家明确一点,那就是以下这些方法,不是需要你全部重写(所以这里说的是重写),这需要根据你想要实现锁的类型(独占锁还是共享锁)来决定你需要重写的方法。

  • tryAcquire(int arg):独占式获取同步状态
  • tryRelease(int arg):独占式释放同步状态
  • tryAcquireShared(int arg):共享式获取同步状态
  • tryReleaseShared(int arg):共享式释放同步状态
  • isHeldExclusively():当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占

我们可以看出来,这些开发人员可以重写的方法,几乎都是围绕着同步状态的获取与释放来提供的,我们来看一下 AQS 中的源码。

    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
    
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }

    protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }

这些方法均未被 abstract 关键字修饰,而笔者在网上看到的好多文章在介绍这部分内容时说的都非常不严谨,直接就说这些方法是待重写的抽象方法,可我们打开 JDK 看 AQS 源码,这里面除了用过 abstract 修饰类,可曾看到任意一个方法是抽象方法了?

这么做是为了保证锁的实现更加方便,在实现某种单一类型的锁的时候不需要重写不相关的方法,比如我现在只想要个互斥锁,而不需要共享锁,那我重写tryAcquireShared(int arg)tryReleaseShared(int arg)有意义吗?所以这些方法都不是被 abstract 修饰过的抽象方法。

但是这些方法的权限修饰符都是 protected 的,这很明显是建议开发人员去 Override 的,而且方法体里面,都仅仅抛出一个 UnsupportedOperationException 异常。

二、模板方法:

  • acquire(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):共享式释放同步状态
  • getQueuedThreads(int arg):获取等待在同步队列上的线程集合

同步器提供的模板方法基本上分为以下三类:

  • 独占式获取与释放同步状态,即 acquire / acquireInterruptibly / tryAcquireNanos / release 这些方法,这些方法将会调用可重写的 tryAcquire / tryRelease 方法,就是我们刚刚在第一部分介绍过的
  • 共享式获取与释放同步状态,即 acquireShared / acquireSharedInterruptibly / tryAcquireSharedNanos / releaseShared 这些方法,这些方法将会调用可重写的 tryAcquireShared / tryReleaseShared 方法,就是我们刚刚在第一部分介绍过的
  • 查询同步队列中的等待线程情况

这些模板方法,可以称之为顶层方法,什么意思呢,举个例子,比如一个独占锁,实际上进行加锁时,会先调用 acquire 方法,然后 acquire 会调用该独占锁的 tryAcquire 方法。具体点,我们拿 ReentrantLock 来看这个加锁的方法调用过程。

Lock lock = new ReentrantLock();
lock.lock(); 

上述代码是一个非常常见的使用 ReentrantLock 来获取同步锁的过程,我们来看看 lock() 方法的源码。

public void lock() {
   sync.lock();
}

调用的是 sync 的 lock()方法,我们先来看一看 sync 是啥。

private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {...}
static final class NonfairSync extends Sync {...}
static final class FairSync extends Sync {...}

静态内部类 Sync 就是 ReentrantLock 的自定义同步器,但是我们发现 Sync 还是一个抽象类,这是因为 ReentrantLock 支持公平锁与非公平锁两种工作模式,具体的同步器由上面的NonfairSyncFairSync来实现,通过构造函数来指定,这里不再过多的展开介绍了。我们直接来看源码。

public ReentrantLock() {
    sync = new NonfairSync();
}
    
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

我们发现,ReentrantLock 默认就是使用非公平锁,我们就拿非公平锁来说吧,刚才说到 ReentrantLock 的 lock() 方法实际上是调用的 sync 的 lock() 方法,那么我们就去看看 NonfairSync 中的 lock() 方法具体实现细节。

final void lock() {
    if (compareAndSetState(0, 1))
         setExclusiveOwnerThread(Thread.currentThread());
    else
         acquire(1);
}

这里解释一下,if 条件内主要是快速的获取锁,如果 CAS 操作成功,那么直接获取到锁。否则就会执行了 AQS 的模板方法啦!那就是 acquire(int arg)方法。我们接着往下看。

public final void acquire(int arg) {
   if (!tryAcquire(arg) &&
      acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
      selfInterrupt();
}

看到了吧,执行acquire(int arg)方法后,会调用tryAcquire(int arg)方法,而这个方法就是 ReentrantLock 重写的方法!到这儿相信你也对模板方法这个设计模式有了更好的了解。

最后我们来了解一下 AQS 的使用示例吧,其实 Doug lea 老爷子在 AQS 的源码的注释中给我们留了一个简单的 demo,大概在第 200 行左右,我们就拿这个来举例吧。

J.U.C包核心AQS(一):快速了解AQS_第1张图片

class Mutex implements Lock, java.io.Serializable {
 *
 *   // 静态内部类,自定义同步器
 *   private static class Sync extends AbstractQueuedSynchronizer {
 *     // 是否处于占用状态
 *     protected boolean isHeldExclusively() {
 *       return getState() == 1;
 *     }
 *
 *     // 当状态为0的时候获取锁
 *     public boolean tryAcquire(int acquires) {
 *       assert acquires == 1; // Otherwise unused
 *       if (compareAndSetState(0, 1)) {
 *         setExclusiveOwnerThread(Thread.currentThread());
 *         return true;
 *       }
 *       return false;
 *     }
 *
 *     // 释放锁,将状态设置为0
 *     protected boolean tryRelease(int releases) {
 *       assert releases == 1; // Otherwise unused
 *       if (getState() == 0) throw new IllegalMonitorStateException();
 *       setExclusiveOwnerThread(null);
 *       setState(0);
 *       return true;
 *     }
 *
 *     // 返回一个Condition,每个condition都包含了一个condition队列
 *     Condition newCondition() { return new ConditionObject(); }
 *
 *     // 反序列化
 *     private void readObject(ObjectInputStream s)
 *         throws IOException, ClassNotFoundException {
 *       s.defaultReadObject();
 *       setState(0); // reset to unlocked state
 *     }
 *   }
 *
 *   // sync把所有脏活累活都干了,我们仅需要将操作代理到Sync上即可.
 *   private final Sync sync = new Sync();
 *
 *   public void lock()                { sync.acquire(1); }
 *   public boolean tryLock()          { return sync.tryAcquire(1); }
 *   public void unlock()              { sync.release(1); }
 *   public Condition newCondition()   { return sync.newCondition(); }
 *   public boolean isLocked()         { return sync.isHeldExclusively(); }
 *   public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
 *   public void lockInterruptibly() throws InterruptedException {
 *     sync.acquireInterruptibly(1);
 *   }
 *   public boolean tryLock(long timeout, TimeUnit unit)
 *       throws InterruptedException {
 *     return sync.tryAcquireNanos(1, unit.toNanos(timeout));
 *   }
 * }}

上述示例中,独占锁 Mutex 是一个自定义同步组件,它在同一时刻只允许一个线程占有 锁。Mutex 中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。在 tryAcquire(int acquires) 方法中,如果经过 CAS 设置成功(同步状态设置为1),则代表获取了同步状态,而在 tryRelease(int releases) 方法中只是将同步状态重置为 0。

用户使用 Mutex 时并不会直接和内部同步器的实现打交道,而是调用 Mutex 提供的方法,在 Mutex 的实现中,以获取锁的 lock() 方法为例,只需要在方法实现中调用同步器的模板方法 acquire(int args) 即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现一个可靠自定义同步组件的门槛。

你可能感兴趣的:(Java并发)