【Java并发】- AbstractQueuedSynchronizer详解(AQS)

AQS简介

AQS(队列同步器),是用于构建锁或者同步组件的基础框架,通过使用一个int型的state变量来表示资源(或者同步状态),同时使用内部的一个FIFO队列来实现多个线程获取资源的排队,在构建相应的锁或者同步组件的时候一般都是以继承AQS并实现其中部分关键的方法来达到锁的功能,同时AQS也提供一些状态的读写的方法如:getState(),setState(int newState),compareAndSetState(int expect, int update)。其中compareAndSetState是以CAS的方式来设置state。同时AQS提供了Exclusive(独占式:只允许一个线程获取资源)和Shared(共享式:允许多个线程同时获取资源)两种不同的锁的获取方式,这样为具体的锁或者同步组件的实现提供了丰富的选择。AQS是面向锁的开发的,它使得锁的开发更加的只管明确,并且不用煞费苦心的去维护同步队列和同步状态等等,而锁则是面向业务开发者,他们只需要使用锁提供的简单明了的方法即可达到锁的效果。


AQS方法介绍

//获取State
protected final int getState();

//设置State
protected final void setState(int newState);

//通过CAS方式设置State,设置成功返回true,失败返回false
protected final boolean compareAndSetState(int expect, int update);

//独占式的获取同步状态,如果成功则从该方法返回,失败则将进入同步队列等待,
//这个方法会去调用tryAcquire(int arg)方法
public final void acquire(int arg);

//与acquire方法相同,不同的是它相应中断,当未获取到同步状态并进入同步队列中后,
//如果该线程被中断,则会返回并抛出InterruptedException
public final void acquireInterruptibly(int arg);

//增加了一个时间限制,如果在规定时间内获取到了同步状态则返回true,否则返回false
public final boolean tryAcquireNanos(int arg, long nanosTimeout);

//独占式的获取同步状态,该方法需要查询当前State是否符合预期并进行CAS设置State
//protected修饰,AQS没有实现,留给子类实现具体的逻辑。
protected boolean tryAcquire(int arg);

//独占式的释放同步状态,该方法会在释放同步状态之后将同步队列中的第一个节点所包含的线程唤醒
//该方法会调用release(int arg)方法去释放同步状态
public final boolean release(int arg);

//独占式的获取同步状态,成功返回true,否则返回false
//protected修饰,AQS没有实现,留给子类实现具体的逻辑。
protected boolean tryRelease(int arg);

//共享式的获取同步状态,与独占式不同的是同一时刻可以有多个线程可以获取到同步状态
public final void acquireShared(int arg);

//响应中断
public final void acquireSharedInterruptibly(int arg);

//增加了超时的限制
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout);

//共享式的获取同步状态,返回大于等于0的值表示获取成功,小于0获取失败(大于0同时还表示还有剩余的资源)
//protected修饰,AQS没有实现,留给子类实现具体的逻辑。
protected int tryAcquireShared(int arg);

//共享式的释放同步状态,该方法会调用tryReleaseShared(int arg),
//释放同步状态成功将同步队列中的头节点移除,并唤醒下一个等待的节点。
public final boolean releaseShared(int arg);

//共享式的释放同步状态,成功释放返回true,反之返回false
//protected修饰,AQS没有实现,留给子类实现具体的逻辑。
protected boolean tryReleaseShared(int arg);

//当前同步器是否在独占式的模式下被线程占用(是否被当前线程所独占)
//子类实现
protected final boolean isHeldExclusively();

可以看到AQS框架提供了很多方法供给子类使用,同时也将同步队列以及同步状态的管理封装起来,并且规定了锁的获取与释放的流程,只是将其中关键的操作留给子类去实现不同的功能。


AQS源码解析

属性

//同步队列的头节点
private transient volatile Node head;
//同步队列的尾节点
private transient volatile Node tail;
//同步状态
private volatile int state;

可以看到这里的同步队列的头尾节点还有同步状态的属性都是由volatile关键字来修饰。配合上AQS中的compareAndSetState(int,expect, int update),compareAndSetHead(Node update),compareAndSetTail(Node expect, Node update)等方法使用无锁CAS自旋的方式来达到线程安全的效果。


Node节点

AQS中以静态内部类的方式维护了一个Node节点的class

同步队列的大致模型:

/*
 *      +------+  prev +-----+       +-----+
 * head |      | <---- |     | <---- |     |  tail
 *      +------+       +-----+       +-----+
 */



static final class Node {
    //共享模式下等待获取同步状态
    static final Node SHARED = new Node();
    //独占式模式下等待获取同步状态
    static final Node EXCLUSIVE = null;
    //当前线程已经取消了(由于中断或者超时)
    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking */
    //当当前线程释放同步状态之后需要去唤醒后继节点
    static final int SIGNAL    = -1;
    //表明线程在Condition队列中等待,在状态被转换之前将不会被作为同步队列节点对待
    static final int CONDITION = -2;
    //表示头节点在共享的释放同步状态后需要传播到其他的节点
    static final int PROPAGATE = -3;
    //等待状态
    volatile int waitStatus;
    //前驱节点
    volatile Node prev;
    //后继节点
    volatile Node next;
    //节点对应的线程
    volatile Thread thread;

    //下一个在condition队列中等待的节点。因为condition队列中只保存独占式的节点,
    //所以用nextWaiter来指向condition中的节点以表示SHRED模式下的节点
    Node nextWaiter;

    //是否为SHARED模式
    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节点最为关键的便是waitStatus,用来表明当前节点所表示的线程处于什么样的等待状态。
这里写图片描述


独占式获取/释放共享状态分析

获取

//独占式获取共享状态(public修饰)
//该方法对中断不敏感,也就是说在队列中等待是如果线程中断,节点也不会从同步队列中移除
public final void acquire(int arg) {
    //1.调用tryAcquire方法尝试获取共享状态
    //2.获取共享状态失败的话调用addWaiter(构造一个节点添加到队列尾部)
    //3.调用acquireQueued方法使得线程进入等待状态,直到其他线程释放资源唤醒当前线程
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
        //中断当前线程
}

addWaiter和enq方法:

//添加到同步队列的尾节点
private Node addWaiter(Node mode) {
    //构建一个节点
    Node node = new Node(Thread.currentThread(), mode);
    // 快速尝试在尾部添加
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        //CAS设置尾部节点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //快速添加失败后进入enq方法
    enq(node);
    return node;
}

private Node enq(final Node node) {
    //CAS+自旋
    for (;;) {
        Node t = tail;
        if (t == null) { // 需要初始化队列
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            //CAS设置尾部节点
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

上面的代码先是直接尝试CAS添加到尾部节点,如果失败的话将进入“死循环”来不断的尝试CAS添加,只有在添加成功后才能返回。这里的CAS+自旋的方式保证了线程的安全性,因为一旦有别的线程也在尝试添加到尾部节点时,如果是普通的LinkedList无法保证线程的安全性,而这里通过CAS的方式,一旦tail节点与预期不一样则返回false进入下一次循环,也就是意味着别的线程正在进行着同样的操作。

acquiredQueued方法:

//让线程进入等待的状态,同时不响应中断
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);
                /帮助GC回收
                p.next = null; // help GC
                //失败标志置为false
                failed = false;
                return interrupted;
            }
            //获取失败的话check是否需要进入阻塞状态,是则进入等待状态并对check中断 
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        //如果失败则取消节点
        if (failed)
            cancelAcquire(node);
    }
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //获取当前线程的等待状态
    int ws = pred.waitStatus;
    //如果是SIGNAL(-1)状态,那么说明当前线程已经设置了如果前驱节点释放同步状态的话将会唤醒自己,
    //所以可以安全的进入等待状态。
    if (ws == Node.SIGNAL)
        return true;
    //如果大于0说明前驱节点已经cancel了,那么就跳过前去节点一直往前找,直到找到等待状态小于等于0的节点。
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        //将node和找到的前驱节点链接起来。
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        //表示当前线程需要一个single信号来唤醒自己,设置前驱节点的等待状态位single(-1)
        //同时使用的的CAS方式可能会失败,所以不阻塞线程因为可能还需要调用这个方法
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

private final boolean parkAndCheckInterrupt() {
    //调用LockSupport的park方法阻塞线程
    LockSupport.park(this);
    return Thread.interrupted();
}

由于每个节点之间没有互相的通信,所以通过判断当前节点的前驱节点是否为头节点来决定自己的行为,如果前驱节点是头节点,那么就尝试去获取同步状态。这同样了维护了同步队列的FIFO的特性,因为当节点被唤醒之后每个节点都会去自发的判断自己的前驱节点是不是头节点,只有当前驱节点是头节点的情况下才会去获取同步状态,否则则进去等待。
流程图:
这里写图片描述

独占式的释放同步状态:

//独占式的释放同步状态
public final boolean release(int arg) {
    //调用tryRelease释放同步资源修改state
    if (tryRelease(arg)) {
        Node h = head;
        //如果head不是null并且头节点的等待状态不是0则唤醒后继节点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
//AQS未实现
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}
//唤醒后继节点
private void unparkSuccessor(Node node) {
    //ws为头节点的等待状态
    int ws = node.waitStatus;
    //如果为负,尝试把等待状态设为0,即使失败也没关系
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    //获取后继节点
    Node s = node.next;
    //后继节点位null或者等待状态大于0(不存在后集节点或者后继节点取消了)
    if (s == null || s.waitStatus > 0) {
        //后继节点置为null
        s = null;
        //从尾部开始往前找,找到不为空且不是头节点并且等待状态是小于等于0的节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    //如果找到了该节点则唤醒该节点
    if (s != null)
        LockSupport.unpark(s.thread);
}

共享式获取/释放共享状态分析

共享式获取与独占式获取的区别主要在于共享式获取允许多个线程同时获取同步状态,一种典型的实现则是读写锁。对于一个文件的读写,多个线程是可以同时获取到读锁的,而此时写线程将布恩那个获取到锁,同样的当写线程获取到锁就等同于独占式的获取到了同步状态,此时其他读写线程均不能获取到同步状态。

共享式获取同步状态:

//共享式获取同步状态
public final void acquireShared(int arg) {
    //调用tryAcquireShared如果返回值小于0则调用doAcquireShared加入同步队列等待
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
//获取同步状态的方法需要子类去实现
//返回一个int,如果大于等于0说明成功获取了同步状态,小于0获取失败
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}
//加入同步队列
private void doAcquireShared(int arg) {
    //调用addWaiter添加节点到队列尾部
    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;
                }
            }
            //判断是否需要阻塞
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

共享式获取主要区别还是在获取同步状态时候的判断不同。

共享式释放同步状态:

//共享式释放同步状态
public final boolean releaseShared(int arg) {
    //调用tryReleaseShared释放同步状态
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
//子类实现
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}

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;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

这里的tryReleaseShared必须保证同步状态安全释放,一般采用循环+CAS实现,因为可能有多个线程都在释放同步状态。

响应中断的同步状态获取

响应中断的共享式同步状态获取和响应中断的独占式同步状态获取的原理基本一致,这里以独占式响应中断的同步状态获取为例:

//当线程被中断时将抛出InterruptedException
public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            //如果被中断,抛出异常
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

可以看到和不响应中断的区别是,这里在检测到中断的时候是直接抛出异常而响应中断的获取同步状态的做法是用一个标志位记录中断并返回。


超时同步状态获取

共享式超时同步状态获取和独占式超时同步状态获取的原理基本一致,这里以独占式超时的同步状态获取为例:

//超时的独占式同步状态获取,响应中断
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    //尝试获取同步状态,失败的话调用doAcquireNanos自旋
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    //Timeout小于0直接忽视
    if (nanosTimeout <= 0L)
        return false;
    //获取截止时间
    final long deadline = System.nanoTime() + nanosTimeout;
    //向同步队列尾部添加节点
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        //自旋
        for (;;) {
            //获取前驱节点
            final Node p = node.predecessor();
            //如果前驱节点是头节点则尝试获取同步状态,成功则设置为头节点,失败判断是否超时
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            //判断是否超时
            nanosTimeout = deadline - System.nanoTime();
            //小于0表示已经超时,直接返回false
            if (nanosTimeout <= 0L)
                return false;
            //如果没有超时,线程进入等待。
            //如果nanosTimeout小于等于spinForTimeoutThreshold将不会进入等待状态
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            //响应中断
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

当线程前驱节点为头节点时尝试获取同步状态,成功则直接返回,失败将重新计算超时时间并进入等待状态,值得注意的是当超时时间小于等于spinForTimeoutThreshold(1000L)的时候将不会进入等待状态,因为当等待的时间非常短的时候无法坐到非常精确的超时等待,所以默认直接进去快速自旋。


小结

AbstractQueuedSynchronizer通过维护了一个int型的state和一个由内部类的Node节点构成的FIFO同步等待队列来样板化的实现了独占式和共享式的同步状态的获取和释放。同时还提供了响应中断的同步状态获取和超时同步状态获取的功能。给锁或者同步组件的的构建提供了非常好的帮助,事实上java.util.concurrent.lock包里面的所有的锁都是通过AQS来实现的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值