Java并发之AQS框架分析总结

    接下来总结一下AQS框架,看来很多博文其中有很多非常有用的信息,通过这些博文再加上自己对源码的总结完成了以下这篇博文,文笔和技术有限,只能当做参考总结,如果有错误或者不明白的地方可以留下评论我们共同学习共同进步。

学习AQS框架之前以下几个基础知识概念得必须明白理解,不然你学习起来可能会很费劲。

什么是CAS 或者说什么是无锁机制的CAS?

    平时我们使用的Synchronized、ReentrantLock这些都是属于独占锁也可以叫做悲观锁,独占锁意味着一次只能有一个线程获取到锁其他线程必须得等待前面获得锁的线程执行完毕。

    无锁机制就是一种乐观的策略,也就是说假设对共享资源之间没有冲突,线程可以不停的执行,如果发生冲突我们则可以使用CAS技术来保证线程安全。

    什么又是CAS呢?CAS就是一种很高级的无锁策略。CAS(Compare And Swap)即比较交换,其核心算法就是给定更新的变量判断值是否和预期值相等,如果是则将传入的值进行修改,否则就是有线程在之前将我们需要更新的变量给修改过了,则不进行操作,但我们可以选择是否继续尝试或者放弃操作。继续操作有个很关键的概率就是自旋锁,一种充分利用CPU的方式来获取更新。

    什么又是自旋锁获取呢?在CAS中如果有其他线程将我们传入的值进行修改过了,我们则读取失败此时就能够使用自旋的操作,即一个while(true)的死循环,直到CAS能够拿到最新的值为之并能够成功更新。下面展示一段非常经典的CAS自旋volatile变量更新值的代码,该代码来源于AtomicInteger的getAndIncrement()方法的源码

 

    上面介绍了CAS无锁机制和自旋锁的概念,接下来我们可以介绍AQS框架啦,先看一张概念图。

Java并发之AQS框架分析总结_第1张图片

AQS框架内部维护了一个Node双向链表的内部类,上图中的head方法我们可以看做正在执行的线程,为什么阻塞队列没有包括head呢,因为head是正在执行的线程,阻塞队列中的线程会依次等待head唤醒其后一个node线程,并将下一个节点设置成头节点执行。接下来我们看看AQS中Node节点的内部类的变量

//标识节点是共享节点
static final Node SHARED = new Node();

//标识节点是独占节点
static final Node EXCLUSIVE = null;

//线程取消竞争锁
static final int CANCELLED = 1;

//用来判断唤醒下一个线程
static final int SIGNAL = -1;

//等待条件
static final int CONDITION = -2;

static final int PROPAGATE = -3;

//节点状态 我们从上面观察我们可知如果waitStatus>0则说明节点取消竞争了,
// 如果waitStatus<0代表节点是有效状态 waitSatus=0说明是初始化的状态
volatile int waitStatus;

//下面两个节点能够用来构建双向链表
//节点的上一个节点
volatile Node prev;

//节点下一个节点
volatile Node next;

//排队此节点的线程。在构造上初始化并在使用后消失。
volatile Thread thread;

    可能没有完全掌握的朋友又要问什么是volatile了,volatile只要记住该变量的修改能够被其他线程所见,如果你不声明 如果我修改int 1 为2此时如果其他线程同时进来则会造成读取时会脏读,会读取到没有更新的int值为1。

    接下来介绍一下AQS类的核心变量

// 头结点,你直接把它当做 当前持有锁的线程 可能是最好理解的
private transient volatile Node head;

// 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
private transient volatile Node tail;

// 这个是最重要的,不过也是最简单的,代表当前锁的状态,0代表没有被占用,大于0代表有线程持有当前锁
// 之所以说大于0,而不是等于1,是因为锁可以重入嘛,每次重入都加上1
private volatile int state;

// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁
// if (currentThread == getExclusiveOwnerThread()) {state++}
private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer

接下来该讲到AQS的核心类了

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

函数的执行流程如下介绍:

  1. tryAacquire()尝试获取锁,如果成功则直接返回true 否则未获取到锁返回false

  2. addWaiter()改方法是如果前面未获取到锁 则将该thread放入阻塞队列的尾部,并标记成独占模式。

  3. acquireQueued()使线程在等待队列中获取资源,如果资源获取成功才返回。如果线程在整个过程中被中断了,则返回true否则返回false

  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后再进行自我的中断,将中断给补上。

接下来介绍一下这几个方法的具体含义。

//重要方法 尝试独占模式获取线程

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

    为什么该方法中并没有具体代码呢,这我们就要想到AQS只是一个框架,一个抽象类,具体实现需要我们去继承,例如ReetrantLock底层就是采用AQS框架来实现的。如果该方法获取失败,则调用以下代码。

//添加到队列尾部
private Node addWaiter(Node mode) {
    //创建一个节点 以独占锁的方式
    Node node = new Node(Thread.currentThread(), mode);
        //pred节点指向tail尾节点
    Node pred = tail;
    //如果尾节点不为空
    if (pred != null) {
        //先关联前驱节点
        node.prev = pred;
        //CAS替换尾节点 如果替换成功
        if (compareAndSetTail(pred, node)) {
            //将原先的尾节点的下一个节点关联到新的尾节点上
            pred.next = node;
            //返回
            return node;
        }
    }
    //1、尾节点为空即队列刚刚初始化或者未初始化 2、在替换尾节点过程中又有新节点进来并提前替换成功了
    //既然前面那种方法不行 咋们就换一种办法入队
    enq(node);
    return node;
}

    该方法的执行流程就是如果未获取到锁资源,我们则将节点线程放入阻塞队列中。先将mode节点转换成独占模式的节点,再构建一个pred变量将尾节点赋值过去,如果尾节点不为空,说明队列已经初始化并有其他节点线程在等待队列中,此时我们就要先将node节点的前驱节点和尾节点关联起来,因为阻塞队列都是从队尾入队,队头出队,再采用CAS操作对尾节点替换,如果替换失败则说明有其他线程成功入队了,则调用enq方法入队。下面代码展示出enq()方法入队

/**
* 将节点入队 必要时进行初始化的操作
* @param node 前面新创建的节点
* @return
*/

private Node enq(final Node node) {
    //非常神奇的无锁机制 自旋锁
    for (; ; ) {
        //拿到当前队列的尾节点
        Node t = tail;
        //如果尾节点为null 说明队列未初始化 则开始进行初始化的操作
        if (t == null) { // Must initialize
            //为什么在这里使用CAS替换而不是直接创建一个头节点呢?
            //因为如果在直线这个判断之前有线程已经创建成功并成功将节点入队了 则会造成队列结构的破坏
            if (compareAndSetHead(new Node()))
                //将尾节点执行新创建的节点
                tail = head;
            //如果已经初始化了尾节点
        } else {
            //则将传进来的节点node的上一个节点绑定尾节点
            node.prev = t;
            //CAS替换node节点为尾节点 如果没成功则循环继续
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

    上面代码展示出神器的自旋锁替换尾节点的过程,在Java并发容器中有很多都使用到了CAS替换volatile自旋的操作。

    上面介绍完addWaiter方法之后,接下来该介绍acquireQueue()方法了。

    避免各位来回翻 下面也有展示

/**
* 以独占模式获取 忽略中断 非常重要的方法  在ReentrackLock的lock方法就是使用acquire(1)
* @param arg
*/
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        //介绍过程中我们说到了如果线程挂起则会调用一个Thread.interripted()方法 该方法会返回线程中断 并重置标志
        //所以我们这里需要重新将标志给打上
        selfInterrupt();
}

    下面介绍acquireQueue()方法

/**
* 前面我们已经将节点存入了队列的尾部 我们这里开始尝试获取锁
* @param node 前面添加进尾部的节点
* @param arg
* @return
*/

final boolean acquireQueued(final Node node, int arg) {
    //成功失败判断
    boolean failed = true;
    try {
        //是否中断
        boolean interrupted = false;
        //自选锁机制尝试获取锁
        for (; ; ) {
            //定义一个p节点用来接收传递过来参数node节点的前驱节点
            final Node p = node.predecessor();
            //如果这个节点是头节点且获取到了锁
            //前面我们也分析过了这个tryAcquire无非就是cas将state状态增加的过程
            if (p == head && tryAcquire(arg)) {
                //将node设置为头节点
                setHead(node);
                //将前面执行完毕的节点设置为null 以便于GC的回收
                p.next = null; // help GC
                failed = false;
                //返回中断状态
                return interrupted;
            }
            //如果p节点不为头节点且又获取不到锁 则会调用 shouldParkAfterFailedAcquire()方法
            //该方法作用是找到一个未被取消的前继节点 并将前继节点的等待状态设置成signel 这样它执行完毕之后便能够唤醒当前线程了
            //parkAndCheckInterrupt() 是通过前面方法找到一个好的节点之后便能够安心去沉睡了(阻塞)
            if (shouldParkAfterFailedAcquire(p, node) &&
                   parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

    如果p节点不是头节点或者tryAcquire()方法获取锁失败

    这执行shouldParkAfterFailedAcquire(p, node) 和 parkAndCheckInterrupt()两个方法

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    //判断前驱节点的状态是否是SINGNAL  如果是SIGNAL则说明下一个节点是需要唤醒的状态则直接返回true
    if (ws == Node.SIGNAL)
        return true;
    //如果前驱节点的状态>0说明节点是前驱节点是被取消的节点 则使用循环队列将该节点放入一个未被取消节点的后面
    //简单点说就是找一个好爹 找到一个好爹之后和他绑定父子关系(将它的节点状态设置为-1)
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        //否则就直接替换节点 并将前驱节点的状态设置为-1 即能够唤醒下一个节点正常工作
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

/**
* 如果前面有节点获得到了锁 调用LockSupport.park将当前线程挂起 等待前驱节点将其唤醒
* @return
*/
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    //返回线程是否被中断 Thread.interrupted()底层调用的是isInterrupted()方法 此方法会重置线程中断标志
    return Thread.interrupted();
}

    真正将线程挂起的方法是parkAndChckInterrupt()方法。

    接下来学习完这几个方法的源码,大家应该对acquire(int arg)方法有了更深刻的了解

/**
* 以独占模式获取 忽略中断 非常重要的方法  在ReentrackLock的lock方法就是使用acquire(1)
* @param arg
*/

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        //介绍过程中我们说到了如果线程挂起则会调用一个Thread.interripted()方法 该方法会返回线程中断 并重置标志
        //所以我们这里需要重新将标志给打上
        selfInterrupt();
}

    为什么需要使用selfInterrupt方法呢,如果线程在执行过程中中断了,因为前面的parkAndCheckInterrupt方法调用返回的是Thread.interrupted()底层调用的isInterrupted()方法,该方法会重置线程的中断标志,所以我们需要重新将线程中断的标志给打上,线程仍然会继续运行。

上面方法介绍的是拿锁过程,接下来介绍的就是解锁过程。

/**
* 会以独占锁的方式释放锁
* @param arg
* @return
*/

public final boolean release(int arg) {
    //尝试释放锁 由子类继承实现
    if (tryRelease(arg)) {
        //拿到头节点
        Node h = head;
        //如果头节点不为空或者头节点的状态不为初始化的状态 则说明有线程占用锁状态并且能够释放锁
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;

}

/**
* 释放锁的同时 并唤醒下一个节点
* @param node the node
*/

private void unparkSuccessor(Node node) {
    //拿到当前节点线程的状态
    int ws = node.waitStatus;
    //如果状态小于0 说明线程是正常状态 能够进行解锁的操作。
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    //同时拿到节点的下一个节点
    Node s = node.next;
    //如果下一个节点为空或者等待状态大于0  说明阻塞队列只有一个节点或者说存在很多节点只是下一个节点为空
    if (s == null || s.waitStatus > 0) {
        s = null;
        //从队尾开始循环 直到循环到当前节点或者尾节点为空为之
        for (Node t = tail; t != null && t != node; t = t.prev)
            //如果节点状态正常则将节点赋值给s 再继续遍历看是否还有前驱节点
            if (t.waitStatus <= 0)
                s = t;
    }
    //如果下一个节点不为空 则直接调用LockSupport.unpark()方法来唤醒下一个节点。
    if (s != null)
        LockSupport.unpark(s.thread);

}

    如果获得锁理解比较深,看释放锁会非常方便,写得也比较简单。接下来AQS框架的几个重要方法给大家介绍了,接下来我们解决一个疑惑。

    为什么unparkSuccessor()释放锁唤醒下一个节点的方法需要从队尾开始往前遍历?

    答:上面代码的从尾部循环是判断这个节点不存在或者这个节点被取消了才开始往前找,下面代码更好的说明出来了这种问题。

//添加到队列尾部

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        //因为在线程如果需要放入等待队列的队尾中时,先关联前驱节点然后再替换节点这样做是使得线程更安全,如果从前往后找的话compareAndSetTail(pred, node)未能够及时替换的过程中,如果有其他线程涌入则新加入的tail是无法被遍历到。
        node.prev = pred;
        //CAS替换尾节点 如果替换成功
        if (compareAndSetTail(pred, node)) {
            //将原先的尾节点的下一个节点关联到新的尾节点上
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

下面看看ReentrantLock类的源码是如何使用AQS框架的,下面列举两个最常用的方法。

直接用的就是AQS框架的获取锁和释放锁的方法。

final void lock() {
    //不公平锁竞争方式 直接CAS设置state状态是否为等待获取锁的方式 如果为0代表是 否则已经有线程获取到了锁
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        //已经有线程获取到锁 尝试使用AQS获取锁
        acquire(1);
}
public void unlock() {
    sync.release(1);
}

博文参考,两篇非常经典的文章,本文只是我用来总结一下学过的AQS框架,如果想了解更深可以看看以下两篇文章,写得非常不错,也可以自己尝试去读一下源码。

      https://javadoop.com/post/AbstractQueuedSynchronizer

      https://www.cnblogs.com/waterystone/p/4920797.html

 

                                                                                                                                                      ----不是非得赢,我只是不想输 。

 

你可能感兴趣的:(java学习)