浅析AQS(1)---独占锁以及共享锁的实现

## 什么是AQS

所谓AQS,指的是AbstractQueuedSynchronizer,它提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,ReentrantLock、Semaphore、ReentrantReadWriteLock,CountDownLatch等并发类均是基于AQS来实现的,具体用法是通过继承AQS实现其模板方法,然后将子类作为同步组件的内部类。

AQS具体的实现方式为通过维护一个state变量,通过调用对应实现的方法来操作state并且根据state的状态来判断是否需要加锁,接下来我们来阅读源码看看AQS中的具体实现

## AQS中的重要变量与方法

### 内部类Node

内部类node即为FIFO的等待队列的具体实现,在线程需要阻塞排队时,便会创建一个node节点对象,该对象持有的变量如下

* waitStatus: 该变量控制当前node的等待状态,枚举如下

  * CALCLED 取消状态,当线程排队被取消时,将节点改为当前状态

  * SIGNAL 就绪状态,表示后面的线程需要被接触阻塞

  * CONDITION 等待状态,正在等待后续的condition

  * PROPAGATE 共享锁的状态下,被多次唤醒状态

* prev,next: 构成队列时需要维护的前后节点的引用

* thread: 持有该节点的线程

* nextWaiter:一个标志该节点是独占节点或者共享节点的标志

### state变量及其set方法get方法compareAndSetState方法

state变量为AQS的核心,所有是否需要加锁的判断以及锁的状态都用改变量来维护,修改与获取变量时需要调用对应的setget方法,该方法是不保证线程安全的.而compareAndSetState方法则是通过cas的方式来修改state变量,是线程安全的修改state变量的方式

### tryAcquire以及acquire方法

tryAcquire方法即尝试以独占锁的方式尝试获取锁,如果获取成功则返回true否则返回false,tryAcquire方法在AQS抽象类中是没有具体的实现的,需要开发者根据自己的需求自定义实现tryAcquire并且定义获取锁的逻辑,后续可以根据ReenTrantLock源码进行分析

acquire方法则是先尝试获取独占锁,如果获取成功则继续进行,如果获取失败则进行排列,我们先来看看源代码

```java

    public final void acquire(int arg) {

        if (!tryAcquire(arg) &&

            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

            selfInterrupt();

    }

```

可以看到acquire的实现方法非常简洁,首先尝试获取锁,如果成功则代码短路直接返回,如果尝试获取锁失败则调用acquireQueued方法将该线程加入阻塞队列,接下来查看一下addWaiter方法

```java

private Node addWaiter(Node mode) {

        Node node = new Node(Thread.currentThread(), mode);

        Node pred = tail;

        if (pred != null) {

            //尾节点不为空即队列不为空

            node.prev = pred;

            //将该节点的前一个节点设置为尾结点,即将该节点插入队列的尾部

            if (compareAndSetTail(pred, node)) {

                //用CAS的方式插入尾结点

                pred.next = node;

                return node;

            }

        }

       //如果尾节点为空,或者CAS插入尾结点失败,则执行enq方法初始化节点,并且自旋插入尾部`

        enq(node);

        return node;

    }

```

可以看到addWaiter方法中就是新建一个Node对象,并且将Node对象插入到FIFO的队列当中去并且返回当前的Node,

下一步我们来看看acquireQueued方法

```java

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; // help GC

                    failed = false;

                    return interrupted;

                }

                //如果没有成功则修改状态并且进入休眠状态

                if (shouldParkAfterFailedAcquire(p, node) &&

                    parkAndCheckInterrupt())

                    interrupted = true;

            }

        } finally {

            if (failed)

                cancelAcquire(node);

        }

    }

```

可以看到在acquireQueued方法中,是执行了一个死循环,自旋的来获取锁,首先获取传入node的上一个节点,如果上一个节点是头节点,则可以尝试获取锁,这里可能有同学不理解,为什么上一个节点是头结点时我们就可以尝试获取锁了,这里后续可以结合release方法来进行分析,如果获取锁成功则将自己的节点设置为头结点

如果当前节点前一个节点不是头结点,或者获取锁失败的情况下,则先执行shouldParkAfterFailedAcquire方法,

```java

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {

        int ws = pred.waitStatus;

        if (ws == Node.SIGNAL)

          //如果状态正确直接返回

            return true;

        if (ws > 0) {

          //如果大于0即状态为CANCEL状态,则将节点前移

            do {

                node.prev = pred = pred.prev;

            } while (pred.waitStatus > 0);

            pred.next = node;

        } else {

            //采用cas的方式将pred节点的状态置位SIGNAL         

            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

        }

        return false;

```

该方法可以理解为,清除当前node节点之前的状态为Cancel即被取消排列的节点,如果当前节点前有状态为Cancel的节点,则把当前节点前移,并且重新自旋尝试,如果当前节点前节点的状态不为SIGNAL,则采用CAS的方式修改为SIGNAL,并且再次自旋

如果当前线程之前的线程状态正确的话,就会继续往下执行parkAndCheckInterrupt方法,该方法源码就不贴了,底层是调用了LockSupport的park方法,即让当前线程阻塞,如果有其他线程唤醒当前线程的话,则会继续执行自旋获取锁的操作.

至此,AQS中以独占锁的方式获取锁的流程已经完成,流程图如下

![acquire1](https://image-xiaoazhai.oss-cn-hangzhou.aliyuncs.com/blog/acquire1.png)

### tryRelease以及release方法

tryRelease方法与tryAcquire方法一样需要开发者自行实现释放锁的逻辑,我们重点来看一下release方法的源码

```java

  public final boolean release(int arg) {

        if (tryRelease(arg)) {

            Node h = head;

            if (h != null && h.waitStatus != 0)

                //头结点不为空且头节点状态不为已完成

                unparkSuccessor(h);

            return true;

        }

        return false;

    }

```

可以看到进行了一波简单的判断来解锁,重点在于这个unparkSuccessor方法

```java

private void unparkSuccessor(Node node) {


    int ws = node.waitStatus;

    if (ws < 0)

        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;

    if (s == null || s.waitStatus > 0) {

        //该节点下一节点为空或者下一节点的状态为已取消

        s = null;

        //循环查找队列中最靠前并且状态不为已取消的节点

        for (Node t = tail; t != null && t != node; t = t.prev)

            if (t.waitStatus <= 0)

                s = t;

    }

    if (s != null)

        //唤醒选中的节点

        LockSupport.unpark(s.thread);

}

```

简言之release方法还是较为简单,在tryRelease方法成功以后,唤醒队列中准备好的节点,这也解释了前面为什么在acquire方法中,前面节点中只要前一个节点是头结点即可尝试获取锁,因为在release方法中并不会移动节点,而是直接唤醒后续第一个可用节点.

### tryAcquireShared以及acquireShared方法

首先要明确acquireShared以及acquire的区别 acquire的方法是以独占锁的方式来获取锁,而acquireShared则是以共享锁的方式来获取锁,

tryAcquireShared方法依旧由开发者实现(try系列的基本都由开发者实现,后面不在赘述),我们来康康acquireShared方法的源码

```java

  public final void acquireShared(int arg) {

        if (tryAcquireShared(arg) < 0)

            doAcquireShared(arg);

    }

```

依旧是如此的简洁,首先要明确一个概念,与tryAcquire不同,tryAcquireShared返回的不是一个Boolean值而是一个int值,该值表示剩余的共享锁次数,如果返回一个负数,则表示获取锁失败,其他表示获取锁成功

接下来分析doAcquireShared方法

```java

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; // help GC

                    if (interrupted)

                        selfInterrupt();

                    failed = false;

                    return;

                }

            }

            //与acquireQueue的相同,校验前方节点状态以及阻塞线程

            if (shouldParkAfterFailedAcquire(p, node) &&

                parkAndCheckInterrupt())

                interrupted = true;

        }

    } finally {

        if (failed)

            cancelAcquire(node);

    }

}

```

其实通过对比可以发现doAcquireShared方法与acquireQueued方法大体上几乎是相同的.基本上都是选择用自旋的方式来尝试获取锁并且阻塞线程,最大的不同点就在于将该节点设置为头结点的方法setHeadAndPropagate方法,我们来深入查看一下源码

```java

private void setHeadAndPropagate(Node node, int propagate) {

    Node h = head; // Record old head for check below

    setHead(node);

    if (propagate > 0 || h == null || h.waitStatus < 0 ||

        (h = head) == null || h.waitStatus < 0) {

        Node s = node.next;

        if (s == null || s.isShared())

            doReleaseShared();

    }

}

```

前两行很好理解,保存一下原来的头,后续进行的一系列判断可能比较难以理解,我们可以一个一个去理一下

* propagate>0,通过前面传参我们可以看到propagate这个变量是tryAcquireShared返回的一个变量,这个变量代表着剩余共享锁的数量,而在调用setHeadAndPropagate已经判断了该变量>=0所以这里判断的实际上就是propagate是否等于0也就是是否还有剩余如果还有剩余的锁,直接短路后面的判断.进行释放锁的操作

* 后续的判断只看这个方法比较难以理解,这里我们贴一下doReleaseShared的源码来统一分析

  ```java

  private void doReleaseShared() {

      for (;;) {

          Node h = head;

          if (h != null && h != tail) {

              int ws = h.waitStatus;

              if (ws == Node.SIGNAL) {

                  //如果头结点的状态正确则尝试更新头结点的status值为0

                  if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))

                      continue;           

                  unparkSuccessor(h);

              }

              //如果头结点的值不为SIGNAL并且头结点的值为0说明已经有其他线程唤醒该头结点后面的节点

               //将该节点置位PROPAGATE-3 可以使后续的线程检测到该线程

              else if (ws == 0 &&

                      !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))

                  continue;             

          }

          if (h == head)

              //如果头结点已经被唤醒过则不需要继续唤醒了

              break;

      }

  }

  ```

* 首先着重解释一下,为什么在propagate==0的情况下还要做后续的判断而不是直接返回, **propagate==0只能代表当时tryAcquireShared后没有共享锁剩余,但之后的时刻很可能又有共享锁释放出来了。**

* 接下来我们来康康为什么需要进行后续的判断,首先分析` h == null || h.waitStatus < 0` 这个判断,可以看到h这个变量是原来的头结点的值,h==null是简单的防空指针判断,而根据doReleaseShared方法可以得知h.waitStatus在运行状态下会被改为0,而被置为负数的情况只有SIGNAL等待状态或者PROPAGATE状态而原来头结点的状态不可能为SIGNAL所以当h.waitStatus<0的时候只有可能是其他的线程也调用了doReleaseShared方法,那么此时应该有可能还有空闲的线程可以使用,那么就去尝试调用doReleaseShared方法尝试唤醒后续的线程

* 继续看,如果propagate > 0不成立,且h.waitStatus < 0不成立,而第二个h.waitStatus < 0成立。注意,第二个h.waitStatus < 0里的h是新head(很可能就是入参node)。第一个h.waitStatus < 0不成立很正常,因为它一般为0(考虑别的线程可能不会那么碰巧读到一个中间状态)。第二个h.waitStatus < 0成立也很正常,因为只要新head不是队尾,那么新head的status肯定是SIGNAL,根据tryAcquireShared方法中可以看出当status等于SIGNAL时,将会尝试唤醒下一个线程,但是此时propagate的值为0大概率是获取锁失败再次阻塞,至于为什么作者要进行这种操作,请看作者的注释

* >The conservatism in both of these checks may cause unnecessary wake-ups, but only when there are multiple racing acquires/releases, so most need signals now or soon anyway.

* 这个方法可能会导致不必要的唤醒,但只有在多个线程竞争acquire或者release的时候才会发生

总结:

* setHeadAndPropagate函数用来设置新head,并在一定情况下调用doReleaseShared。

  调用doReleaseShared时,可能会造成acquire thread不必要的唤醒。个人认为,作者这么写,是为了防止一些未知的bug,毕竟当一个线程刚获得共享锁后,它的后继很可能也能获取。

* 可以猜想,doReleaseShared的实现必须是无伤大雅的,因为有时调用它是没有必要的。

* PROPAGATE状态存在的意义是它的符号和SIGNAL相同,都是负数,所以能用< 0检测到。因为线程刚被唤醒,但还没设置新head前,当前head的status是0,所以把0变成PROPAGATE,好让被唤醒线程可以检测到。

到此为止AQS中的几个比较核心的方法以及线程阻塞和唤醒的工作流程都已经看过了一遍,对AQS的概念也有了一些了解,后续我们可以结合ReenTrantLock,ReentrantReadWriteLock,Semaphore的源码来了解AQS的实现过程


![我的公众号](https://image-xiaoazhai.oss-cn-hangzhou.aliyuncs.com/blog/qrcode_for_gh_d6d50bf01095_430.jpg)

你可能感兴趣的:(浅析AQS(1)---独占锁以及共享锁的实现)