AQS(AbstractQueuedSynchronizer)

AQS

【原创】Java并发编程系列14 | AQS源码分析

图解AQS:

我画了35张图就是为了让你深入 AQS

AbstractQueuedSynchronizer是Java并发包java.util.concurrent的核心基础组件,是实现Lock的基础。

AQS AbstractQueuedSynchronizer  抽象队列同步器 Java 并发高频面试题:聊聊你对 AQS 的理解?

AbstractQueuedSynchronizer是Java并发包java.util.concurrent的核心基础组件,是实现Lock的基础。

AQS实现了对同步状态的管理,以及对阻塞线程进行排队、等待通知等,本文将从源码角度深入理解AQS的实现原理。

一、AQS类结构

1.属性

// 属性

private transient volatile Node head;// 同步队列头节点

private transient volatile Node tail;// 同步队列尾节点

private volatile int state;// 当前锁的状态:0代表没有被占用,大于0代表锁已被线程占用(锁可以重入,每次重入都+1)

private transient Thread exclusiveOwnerThread; // 继承自AbstractOwnableSynchronizer 持有当前锁的线程

2.方法

// 锁状态

getState()// 返回同步状态的当前值;

setState(int newState)// 设置当前同步状态;

compareAndSetState(int expect, int update)// 使用CAS设置当前状态,保证状态设置的原子性;

    

// 独占锁

acquire(int arg)// 独占式获取同步状态,如果获取失败则插入同步队列进行等待;

acquireInterruptibly(int arg)// 与acquire(int arg)相同,但是该方法响应中断;

tryAcquireNanos(int arg,long nanos)// 在acquireInterruptibly基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false;

release(int arg)// 独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中头节点的下一个节点包含的线程唤醒;

    

// 共享锁

acquireShared(int arg)// 共享式获取同步状态,与独占式的区别在于同一时刻有多个线程获取同步状态;

acquireSharedInterruptibly(int arg)// 在acquireShared方法基础上增加了能响应中断的功能;

tryAcquireSharedNanos(int arg, long nanosTimeout)// 在acquireSharedInterruptibly基础上增加了超时等待的功能;

releaseShared(int arg)// 共享式释放同步状态;

    

// AQS使用模板方法设计模式

// 模板方法,需要子类实现获取锁/释放锁的方法

tryAcquire(int arg)// 独占式获取同步状态;

tryRelease(int arg)// 独占式释放同步状态;

tryAcquireShared(int arg)// 共享式获取同步状态;

tryReleaseShared(int arg)// 共享式释放同步状态;

3.内部类

// 同步队列的节点类

staticfinalclass Node {

volatile Node prev;// 当前节点/线程的前驱节点

volatile Node next;// 当前节点/线程的后继节点

volatile Thread thread;// 每一个节点对应一个线程

volatileint waitStatus;// 节点状态

staticfinalint CANCELLED = 1;// 节点状态:此线程取消了争抢这个锁

staticfinalint SIGNAL = -1;// 节点状态:当前node的后继节点对应的线程需要被唤醒(表示后继节点的状态)

staticfinalint CONDITION = -2;// 节点状态:当前节点进入等待队列中

staticfinalint PROPAGATE = -3;// 节点状态:表示下一次共享式同步状态获取将会无条件传播下去

Node nextWaiter;// 共享模式/独占模式

staticfinal Node SHARED = new Node();// 共享模式

staticfinal Node EXCLUSIVE = null;// 独占模式

}

二、实现原理    

AQS中 维护了一个volatile int state(代表共享资源)和一个FIFO双向线程等待队列(多线程争用资源被阻塞时会进入此队列)。

这里volatile能够保证多线程下的可见性,当state=1则代表当前对象锁已经被占有,其他线程来加锁时则会失败,加锁失败的线程会被放入一个FIFO的等待队列中,比列会被UNSAFE.park()操作挂起,等待其他获取锁的线程释放锁才能够被唤醒。

另外state的操作都是通过CAS来保证其并发修改的安全性。

AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作。

如果当前线程获取锁失败时,AQS会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会park当前线程;当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。

队列结构

同步队列由双向链表实现,AQS持有头尾指针(head/tail属性)来管理同步队列。

节点的数据结构,即AQS的静态内部类Node,包括节点对应的线程、节点的等待状态等信息。

具体原理我们可以用一张图来简单概括:

         

AQS 中提供了很多关于锁的实现方法,

  • getState():获取锁的标志state值

  • setState():设置锁的标志state值

  • tryAcquire(int):独占方式获取锁。尝试获取资源,成功则返回true,失败则返回false。

  • tryRelease(int):独占方式释放锁。尝试释放资源,成功则返回true,失败则返回false。

AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作。

如果当前线程获取锁失败时,AQS会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会park当前线程;当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。

入队代码

/**

* 1.线程抢锁失败后,封装成node加入队列

* 2.队列有tail,可直接入队。

* 2.1入队时,通过CAS将node置为tail。CAS操作失败,说明被其它线程抢先入队了,node需要通过enq()方法入队。

* 3.队列没有tail,说明队列是空的,node通过enq()方法入队,enq()会初始化head和tail。

*/

private Node addWaiter(Node mode) {

Node node = new Node(Thread.currentThread(), mode);// 线程抢锁失败后,封装成node加入队列

Node pred = tail;

if (pred != null) {// 如果有tail,node加入队尾

node.prev = pred;

if (compareAndSetTail(pred, node)) {// 通过CAS将node置为tail。CAS操作失败,说明被其它线程抢先入队了,node需要通过enq()方法入队。

pred.next = node;

return node;

}

}

enq(node);// 如果没有tail,node通过enq()方法入队。

return node;

}

/**

* 1.通过自旋的方式将node入队,只有node入队成功才返回,否则一直循环。

* 2.如果队列为空,初始化head/tail,初始化之后再次循环到else分支,将node入队。

* 3.node入队时,通过CAS将node置为tail。CAS操作失败,说明被其它线程抢先入队了,自旋,直到成功。

*/

private Node enq(final Node node) {

for (;;) {// 自旋:循环入列,直到成功

Node t = tail;

if (t == null) {

// 初始化head/tail,初始化之后再次循环到else分支,将node入队

if (compareAndSetHead(new Node()))

tail = head;

} else {

// node入队

node.prev = t;

if (compareAndSetTail(t, node)) {// 通过CAS将node置为tail。操作失败,说明被其它线程抢先入队了,自旋,直到成功。

t.next = node;

return t;

}

}

}

}

获取锁

/**

* 1.当前线程通过tryAcquire()方法抢锁。

* 2.线程抢到锁,tryAcquire()返回true,结束。

* 3.线程没有抢到锁,addWaiter()方法将当前线程封装成node加入同步队列,并将node交由acquireQueued()处理。

*/

public final void acquire(int arg) {

    if (!tryAcquire(arg) && // 子类的抢锁操作,下文有解释

        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 子类抢锁失败进入队列中,重点方法,下文详细讲解

        selfInterrupt();

}

/**

* 需要子类实现的抢锁的方法

* 目前可以理解为通过CAS修改state的值,成功即为抢到锁,返回true;否则返回false。

* 之后重入锁ReentrantLock、读写锁ReentrantReadWriteLock中会详细讲解。

*/

protected boolean tryAcquire(int arg) {

    thrownew UnsupportedOperationException();

}

/**

* 上文介绍过的入队操作,线程抢锁失败,将当前线程封装成node加入同步队列,并返回node

* Node.EXCLUSIVE-表示独占锁,先不用关注

*/

addWaiter(Node.EXCLUSIVE)

/**

* 重点方法!!

* 1.只有head的后继节点能去抢锁,一旦抢到锁旧head节点从队列中删除,next被置为新head节点。

* 2.如果node线程没有获取到锁,将node线程挂起。

* 3.锁释放时head节点的后继节点唤醒,唤醒之后继续for循环抢锁。

*/

final boolean acquireQueued(final Node node, int arg) {

    boolean failed = true;

    try {

        boolean interrupted = false;

        for (;;) {// 注意这里是循环

            /*

             * 1.node的前置节点是head时,可以调用tryAcquire()尝试去获取锁,获取锁成功则将node置为head

             * 注意:只有head的后继节点能去抢锁,一旦抢到锁旧head节点从队列中删除,next被置为新head节点

             * 2.node线程没有获取到锁,继续执行下面另一个if的代码

             *  此时有两种情况:1)node不是head的后继节点,没有资格抢锁;2)node是head的后继节点但抢锁没成功

             */

            final Node p = node.predecessor();

            if (p == head && tryAcquire(arg)) {

                setHead(node);

                p.next = null; // help GC

                failed = false;

                return interrupted;

            }

           

            /*

             * shouldParkAfterFailedAcquire(p, node):通过前置节点pred的状态waitStatus 来判断是否可以将node节点线程挂起

             * parkAndCheckInterrupt():将当前线程挂起

             * 1.如果node前置节点p.waitStatus==Node.SIGNAL(-1),直接将当前线程挂起,等待唤醒。

             *      锁释放时会将head节点的后继节点唤醒,唤醒之后继续for循环抢锁。

             * 2.如果node前置节点p.waitStatus<=0但是不等于-1,

             *      1)shouldParkAfterFailedAcquire(p, node)会将p.waitStatus置为-1,并返回false;

             *      2)进入一下次for循环,先尝试抢锁,没获取到锁则又到这里,此时p.waitStatus==-1,就会挂起当前线程。

             *  3.如果node前置节点p.waitStatus>0,

             *      1)shouldParkAfterFailedAcquire(p, node)为node找一个waitStatus<=0的前置节点,并返回false;

             *      2)继续for循环

             */

            if (shouldParkAfterFailedAcquire(p, node) &&

                parkAndCheckInterrupt())

                interrupted = true;

        }

    } finally {

        if (failed)

            cancelAcquire(node);

    }

}

/**

* 通过前置节点pred的状态waitStatus 来判断是否可以将node节点线程挂起

* pred.waitStatus==Node.SIGNAL(-1)时,返回true表示可以挂起node线程,否则返回false

* @param pred node的前置节点

* @param node 当前线程节点

*/

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

    int ws = pred.waitStatus;

    if (ws == Node.SIGNAL)

        returntrue;

    if (ws > 0) {

        /*

         * waitStatus>0 ,表示节点取消了排队

         * 这里检测一下,将不需要排队的线程从队列中删除(因为同步队列中保存的是等锁的线程)

         * 为node找一个waitStatus<=0的前置节点pred

         */

        do {

            node.prev = pred = pred.prev;

        } while (pred.waitStatus > 0);

        pred.next = node;

    } else {

        // 此时pred.waitStatus<=0但是不等于-1,那么将pred.waitStatus置为Node.SIGNAL(-1)

        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

    }

    returnfalse;

}

/**

* 将当前线程挂起

* LockSupport.park()挂起当前线程;LockSupport.unpark(thread)唤醒线程thread

*/

private final boolean parkAndCheckInterrupt() {

    LockSupport.park(this);// 将当前线程挂起

    return Thread.interrupted();

}

释放锁

/**

* 释放锁之后,唤醒head的后继节点next。

* 回顾上文讲的acquireQueued()方法,next节点会进入for循环的下一次循环去抢锁

*/

public final boolean release(int arg) {

if (tryRelease(arg)) {// 子类实现的释放锁的方法,下文有讲解

Node h = head;

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

unparkSuccessor(h);// 唤醒node节点(也就是head)的后继节点,下文有讲解

returntrue;

}

returnfalse;

}

/**

* 需要子类实现的释放锁的方法,对应于tryAcquire()

* 目前可以理解为将state的值置为0。

* 之后重入锁ReentrantLock、读写锁ReentrantReadWriteLock中会详细讲解。

*/

protected boolean tryRelease(int arg) {

thrownew UnsupportedOperationException();

}

/**

* 唤醒node节点(也就是head)的后继节点

*/

private void unparkSuccessor(Node node) {

int ws = node.waitStatus;

if (ws < 0)

compareAndSetWaitStatus(node, ws, 0);

Node s = node.next;// 正常情况,s就是head.next节点

/*

* 有可能head.next取消了等待(waitStatus==1)

* 那么就从队尾往前找,找到waitStatus<=0的所有节点中排在最前面的去唤醒

*/

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);// 唤醒s节点的线程去抢锁

}

  • 使用Node实现FIFO队列,可以用于构建锁或者其它同步装置的基础框架;

  • 利用了一个int类型表示状态;

  • 使用方法是继承;

  • 子类通过继承并通过实现它的方法管理锁的状态,对应AQS中acquire和release的方法操纵锁状态;

  • 可以同步实现排它锁和共享锁模式(独占、共享)。

Java中的锁实现

3.1、队列同步器(AQS)

队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架。

3.1.1、它使用了一个int成员变量表示同步状态。

image.png

3.1.2、通过内置的FIFO双向队列来完成获取锁线程的排队工作。

  • 同步器包含两个节点类型的应用,一个指向头节点,一个指向尾节点,未获取到锁的线程会创建节点线程安全(compareAndSetTail)的加入队列尾部。同步队列遵循FIFO,首节点是获取同步状态成功的节点。

    未获取到锁的线程将创建一个节点,设置到尾节点。如下图所示:

  • 首节点的线程在释放锁时,将会唤醒后继节点。而后继节点将会在获取锁成功时将自己设置为首节点。如下图所示:

    3.1.3、独占式/共享式锁获取

独占式:有且只有一个线程能获取到锁,如:ReentrantLock。

共享式:可以多个线程同时获取到锁,如:CountDownLatch

独占式

  • 每个节点自旋观察自己的前一节点是不是Header节点,如果是,就去尝试获取锁。

  • 独占式锁获取流程:

image.png

共享式:

  • 共享式与独占式的区别:

    image.png

  • 共享锁获取流程:

AQS UML

AQS实现

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值