1. AQS是什么?

AQS界说了一套多线程拜访同享资源的同步器结构,许多同步类完成都依赖于它,如常用的ReentrantLock。 简单来说,AQS界说了一套结构,来完成同步类

2. AQS中心思维

2.1 基本结构

AQS的中心思维是关于同享资源,保护一个双端行列来管理线程,行列中的线程顺次获取资源,获取不到的线程进入行列等候,直到资源开释,行列中的线程顺次获取资源。 AQS的基本结构如图所示:

【后端面经-Java】AQS详解

2.1.1 资源state

state变量表明同享资源,通常是int类型。

  1. 拜访办法 state类型用户无法直接进行修正,而需求借助于AQS供给的办法进行修正,即getState()setState()compareAndSetState()等。
  2. 拜访类型 AQS界说了两种资源拜访类型:
    • 独占(Exclusive):一个时间点资源只能由一个线程占用;
    • 同享(Share):一个时间点资源能够被多个线程共用。

2.1.2 CLH双向行列

CLH行列是一种根据逻辑行列非线程饥饿的自旋公平锁,详细介绍可参阅此篇博客。CLH中每个节点都表明一个线程,处于头部的节点获取资源,而其他资源则等候。

  1. 节点结构 Node类源码如下所示:
static final class Node {
    // 形式,分为同享与独占
    // 同享形式
    static final Node SHARED = new Node();
    // 独占形式
    static final Node EXCLUSIVE = null;        
    // 结点状况
    // CANCELLED,值为1,表明当时的线程被撤销
    // SIGNAL,值为-1,表明当时节点的后继节点包含的线程需求运转,也便是unpark
    // CONDITION,值为-2,表明当时节点在等候condition,也便是在condition行列中
    // PROPAGATE,值为-3,表明当时场景下后续的acquireShared能够得以履行
    // 值为0,表明当时节点在sync行列中,等候着获取锁
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;        
    // 结点状况
    volatile int waitStatus;        
    // 前驱结点
    volatile Node prev;    
    // 后继结点
    volatile Node next;        
    // 结点所对应的线程
    volatile Thread thread;        
    // 下一个等候者
    Node nextWaiter;
    // 结点是否在同享形式下等候
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
    // 获取前驱结点,若前驱结点为空,抛出反常
    final Node predecessor() throws NullPointerException {
        // 保存前驱结点
        Node p = prev; 
        if (p == null) // 前驱结点为空,抛出反常
            throw new NullPointerException();
        else // 前驱结点不为空,回来
            return p;
    }
    // 无参结构办法
    Node() {    // Used to establish initial head or SHARED marker
    }
    // 结构办法
        Node(Thread thread, Node mode) {    // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }
    // 结构办法
    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

Node的办法和属性值如图所示:

【后端面经-Java】AQS详解

其间,

  • waitStatus表明当时节点在行列中的状况;
  • thread表明当时节点表明的线程;
  • prevnext别离表明当时节点的前驱节点和后继节点;
  • nextWaiterd当存在CONDTION行列时,表明一个condition状况的后继节点。
  1. waitStatus 结点的等候状况是一个整数值,详细的参数值和含义如下所示:
  • 1CANCELLED,表明节点获取锁的请求被撤销,此刻节点不再请求资源;
  • 0,是节点初始化的默认值;
  • -1SIGNAL,表明线程做好预备,等候资源开释;
  • -2CONDITION,表明节点在condition等候行列中,等候被唤醒而进入同步行列;
  • -3PROPAGATE,当时线程处于同享形式下的时候会运用该字段。

2.2 AQS模板

AQS供给一系列结构,作为一个完好的模板,自界说的同步器只需求完成资源的获取和开释就能够,而不需求考虑底层的行列修正、状况改变等逻辑。 运用AQS完成一个自界说同步器,需求完成的办法:

  • isHeldExclusively():该线程是否独占资源,在运用到condition的时候会完成这一办法;
  • tryAcquire(int):独占形式获取资源的办法,成功获取回来true,不然回来false;
  • tryRelease(int):独占形式开释资源的办法,成功获取回来true,不然回来false;
  • tryAcquireShared(int):同享形式获取资源的办法,成功获取回来true,不然回来false;
  • tryReleaseShared(int):同享形式开释资源的办法,成功获取回来true,不然回来false;

一般来说,一个同步器是资源独占形式或许资源同享形式的其间之一,因而tryAcquire(int)tryAcquireShared(int)只需求完成一个即可,tryRelease(int)tryReleaseShared(int)同理。 可是同步器也能够完成两种形式的资源获取和开释,从而完成独占和同享两种形式。

3. 源码剖析

3.1 acquire(int)

acquire(int)是获取源码部分的顶层进口,源码如下所示:

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

这段代码展示的资源获取流程如下:

  • tryAcquire()测验直接去获取资源;获取成功则直接回来
  • 假如获取失败,则addWaiter()将该线程参加等候行列的尾部,并标记为独占形式;
  • acquireQueued()使线程阻塞在等候行列中获取资源,一直获取到资源后才回来。

简单总结便是:

  • 获取资源;
  • 失败就排队;
  • 排队要等候。

从上文的描绘可见重要的办法有三个:tryAquire()addWaiter()acquireQueued()。下面将逐一剖析其源码:

3.1.1 tryAcquire(int)

tryAcquire(int)是获取资源的办法,源码如下所示:

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

该办法是一个空办法,需求自界说同步器完成,因而在运用AQS完成同步器时,需求重写该办法。这也是“自界说的同步器只需求完成资源的获取和开释就能够”的表现。

3.1.2 addWaiter(Node.EXCLUSIVE)

addWaiter(Node.EXCLUSIVE)是将线程参加等候行列的尾部,源码如下所示:

private Node addWaiter(Node mode) {
    //以给定形式结构结点。mode有两种:EXCLUSIVE(独占)和SHARED(同享)
    //aquire()办法是独占形式,因而直接运用Exclusive参数。
    Node node = new Node(Thread.currentThread(), mode);
    //测验快速办法直接放到队尾。
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //上一步失败则经过enq入队。
    enq(node);
    return node;
}

首要,运用形式将当时线程结构为一个节点,然后测验将该节点放入队尾,假如成功则回来,不然调用enq(node)将节点放入队尾,最终回来当时节点的位置指针。 其间,enq(node)办法是将节点参加行列的办法,源码如下所示:

private Node enq(final Node node) {
    for (;;) { // 无限循环,保证结点能够成功入行列
        // 保存尾结点
        Node t = tail;
        if (t == null) { // 尾结点为空,即还没被初始化
            if (compareAndSetHead(new Node())) // 头节点为空,并设置头节点为重生成的结点
                tail = head; // 头节点与尾结点都指向同一个重生结点
        } else { // 尾结点不为空,即现已被初始化过
            // 将node结点的prev域衔接到尾结点
            node.prev = t; 
            if (compareAndSetTail(t, node)) { // 比较结点t是否为尾结点,若是则将尾结点设置为node
                // 设置尾结点的next域为node
                t.next = node; 
                return t; // 回来尾结点
            }
        }
    }
}

3.1.3 acquireQueued(Node node, int arg)

这部分源码是将线程阻塞在等候行列中,线程处于等候状况,直到获取到资源后才回来,源码如下所示:

// sync行列中的结点在独占且疏忽中止的形式下获取(资源)
final boolean acquireQueued(final Node node, int arg) {
    // 标志
    boolean failed = true;
    try {
        // 中止标志
        boolean interrupted = false;
        for (;;) { // 无限循环
            // 获取node节点的前驱结点
            final Node p = node.predecessor(); 
            if (p == head && tryAcquire(arg)) { // 前驱为头节点而且成功获得锁
                setHead(node); // 设置头节点
                p.next = null; // help GC
                failed = false; // 设置标志
                return interrupted; 
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())//
                //shouldParkAfterFailedAcquire只有当该节点的前驱结点的状况为SIGNAL时,才能够对该结点所封装的线程进行park操作。不然,将不能进行park操作。
                //parkAndCheckInterrupt首要履行park操作,即禁用当时线程,然后回来该线程是否现已被中止
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

acquireQueued(Node node, int arg)办法的首要逻辑如下:

  • 获取node节点的前驱结点,判别前驱节点是不是头部节点head,有没有成功获取资源。
  • 假如前驱结点是头部节点head而且获取了资源,说明自己应该被唤醒,设置该节点为head节点等候下一个获得资源;
  • 假如前驱节点不是头部节点或许没有获取资源,则判别是否需求park当时线程,
    • 判别前驱节点状况是不是SIGNAL,是的话则park当时节点,不然不履行park操作;
  • park当时节点之后,当时节点进入等候状况,等候被其他节点unpark操作唤醒。然后重复此逻辑过程。

3.2 release(int)

release(int)是开释资源的顶层进口办法,源码如下所示:

public final boolean release(int arg) {
    if (tryRelease(arg)) { // 开释成功
        // 保存头节点
        Node h = head; 
        if (h != null && h.waitStatus != 0) // 头节点不为空而且头节点状况不为0
            unparkSuccessor(h); //开释头节点的后继结点
        return true;
    }
    return false;
}

release(int)办法的首要逻辑如下:

  • 测验开释资源,假如开释成功则回来true,不然回来false
  • 开释成功之后,需求调用unparkSuccessor(h)唤醒后继节点。

下面介绍两个重要的源码函数:tryRelease(int)unparkSuccessor(h)

3.2.1 tryRelease(int)

tryRelease(int)是开释资源的办法,源码如下所示:

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

这部分是需求自界说同步器自己完成的,要注意的是回来值需求为boolean类型,表明开释资源是否成功。

3.2.2 unparkSuccessor(h)

unparkSuccessor(h)是唤醒后继节点的办法,源码如下所示:

private void unparkSuccessor(Node node) {
    //这儿,node一般为当时线程地点的结点。
    int ws = node.waitStatus;
    if (ws < 0)//置零当时线程地点的结点状况,允许失败。
        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;//找到下一个需求唤醒的结点s
    if (s == null || s.waitStatus > 0) {//假如为空或已撤销
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找。
            if (t.waitStatus <= 0)//从这儿能够看出,<=0的结点,都是还有用的结点。
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);//唤醒
}

这部分首要是查找第一个还处于等候状况的节点,将其唤醒; 查找顺序是从后往前找,这是由于CLH行列中的prev链是强共同的,从后往前找更加安全,而next链由于addWaiter()办法和cancelAcquire()办法的存在,不是强共同的,因而从前往后找可能会出现问题。这部分的详细解说能够参阅参阅文献-1

3.3 acquireShared(int)和releaseShared(int)

3.3.1 acquireShared(int)

是运用同享形式获取同享资源的顶层进口办法,源码如下所示:

public final void acquireShared(int arg) {
     if (tryAcquireShared(arg) < 0)
         doAcquireShared(arg);
}

流程如下:

  • 经过tryAcquireShared(arg)测验获取资源,假如获取成功则直接回来;
  • 假如获取资源失败,则调用doAcquireShared(arg)将线程阻塞在等候行列中,直到被unpark()/interrupt()并成功获取到资源才回来。

其间,tryAcquireShared(arg)是获取同享资源的办法,也是需求用户自己完成。

doAcquireShared(arg)是将线程阻塞在等候行列中,直到获取到资源后才回来,详细流程和acquireQueued()办法类似, 源码如下所示:

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) {//假如到head的下一个,由于head是拿到资源的线程,此刻node被唤醒,很可能是head用完资源来唤醒自己的
                int r = tryAcquireShared(arg);//测验获取资源
                if (r >= 0) {//成功
                    setHeadAndPropagate(node, r);//将head指向自己,还有剩下资源能够再唤醒之后的线程
                    p.next = null; // help GC
                    if (interrupted)//假如等候过程中被打断过,此刻将中止补上。
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            //判别状况,寻找安全点,进入waiting状况,等着被unpark()或interrupt()
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

3.3.2 releaseShared(int)

releaseShared(int)是开释同享资源的顶层进口办法,源码如下所示:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {//测验开释资源
        doReleaseShared();//唤醒后继结点
        return true;
    }
    return false;
}

流程如下:

  • 运用tryReleaseShared(arg)测验开释资源,假如开释成功则回来true,不然回来false;
  • 假如开释成功,则调用doReleaseShared()唤醒后继节点。

下面介绍一下doReleaseShared()办法,源码如下所示:

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                unparkSuccessor(h);//唤醒后继
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;
        }
        if (h == head)// head发生变化
            break;
    }
}

4. 面试问题模拟

Q:AQS是接口吗?有哪些没有完成的办法?看过相关源码吗?

AQS界说了一个完成同步类的结构,完成办法首要有tryAquiretryRelease,表明独占形式的资源获取和开释,tryAquireSharedtryReleaseShared表明同享形式的资源获取和开释。 源码剖析如上文所述。

参阅资料

  1. Java并发之AQS详解
  2. JUC锁: 锁中心类AQS详解
  3. 从ReentrantLock的完成看AQS的原理及应用