jdk锁知识(四)—— AQS

1、AQS是什么,实现的功能是什么,其在Lock锁和整个并发包中地位

AQS即AbstractQueuedSynchronizer,是一个用于构建锁和同步器的框架。它提供了两个队列,保证了多个线程使用同一资源时锁竞争和锁等待的可行性。jdk并发包中很多锁都依赖该对象构建具有特殊功能的锁。下面是其在并发包中的使用情况图,可以看到几乎所有常见锁都引用了它,甚至线程池中也用它来维护池中线程创建销毁等相关操作。

2、AQS理论介绍

AQS使用一个整型的变量表示当前锁的状态,多线程对锁的竞争,其实就是对状态变量的竞争。AQS中使用两个双向队列维护锁的竞争以及线程的等待,双向队列有一个头指针和一个尾指针。大致图形如下:

锁同步队列是直接具有竞争锁资格的队列,而条件队列则是要先唤醒后,才具有竞争锁的资格。这里两个队列一定要分请,有的时候通过字面意思我们会误以为AQS只有一个同步队列。

AQS锁又分为共享锁和独占锁。独占锁是同步队列中每个节点自旋观察自己的前一节点是不是Header节点,如果是,就去尝试获取锁。而共享锁则是首节点获取锁后,会将锁的状态传播下去,这样就可以有多个节点获取到锁。

这里还有一个公平锁和非公平锁的概念,公平锁主要是每次线程过来都是直接对队列尾部创建节点,然后等待获取锁执行。非公平锁则是线程来了直接尝试获取锁,获取成功就执行,获取失败之后再创建节点放置队列尾部等待获取锁执行。

3、AQS源码分析

先来整体看一下,方法由于太多,所以我是挑选了重载比较多的核心方法进行展示:

内部类:Node、ConditionObject

变量:head、headOffset、nextOffset、state、stateOffset、tail、tailOffSet、unsafe、waitStatusOffset

方法:acquire、addWaiter、doAcquire、tryAcquire、release

可以看到AQS中同步和等待队列通过Node封装,且等待队列与ConditionObject内部类相关。AQS本身则大致通过acquire、release方法外加state、head、tail变量实现了锁的功能。下面先来看一下两个内部类的信息。首先是Node内部类:

static final class Node {
        static final Node SHARED = new Node(); // 标识节点是否等待获取共享锁 
        static final Node EXCLUSIVE = null; // 标识节点是否等待获取排他锁 
        static final int CANCELLED =  1; // waitStatus 为1 标识当前线程取消竞争锁 
        static final int SIGNAL    = -1; // waitStatus 为-1 标识此Node后继节点要被唤醒 
        static final int CONDITION = -2; // waitStatus 为-2 标识当前节点在等待队列 
        // waitStatus 为-3 表示当前节点为传播节点
        static final int PROPAGATE = -3;
        // 节点当前状态值,可以为1 -1 -2 -3,初始化为0
        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内部类很简单,封装了线程节点可能有的状态以及锁的类型,后续各个线程节点所处的状态就由这些变量进行标明。这里对于waitStatus变量各个值的含义一定要了解清楚,它是你能否在源码追踪过程中看懂源码的关键。这里我详细讲下:

-1: 这个状态表示要唤醒后续节点,表面意思很清晰。同步队列新入队节点,如果尝试获取锁失败,就会去尝试将它的前驱节点设置为-1,然后挂起当前节点

-2:进入等待队列中的节点状态,这个好理解不多说

-3:这个表示当前节点是传播节点,这个一定要注意。一般是在释放共享锁的时候,如果当前线程已经释放锁,就会将其从0转换成-3

0:初始态,一般节点刚创建还有就是头节点释放锁之后处于的状态

2:取消态,很简单就是节点被取消

下面看一下ConditionObject内部类:

public class ConditionObject implements Condition, java.io.Serializable {
        private transient Node firstWaiter; //等待队列首节点
        private transient Node lastWaiter; //等待队列尾节点

        //添加等待节点要等待队列尾部,并返回添加的节点
        private Node addConditionWaiter() {            。。。        }

        // 从等待队列首节点开始,唤醒一个等待状态的节点
        private void doSignal(Node first) {            。。。        }

        // 从等待队列首节点开始,唤醒所有等待状态的节点
        private void doSignalAll(Node first) {            。。。        }

        //取消等待队列中所有取消态的节点链接
        private void unlinkCancelledWaiters() {            。。。        }

        // 封装私有唤醒方法给外界使用
        public final void signal() {            。。。        }

        // 封装私有唤醒方法给外界使用
        public final void signalAll() {            。。。        }

        // 获取锁的线程重新进入等待状态,并释放锁
        public final void awaitUninterruptibly() {            。。。        }

        // 等待阻塞时中断、当前线程重复中断、重复尝试获取锁时中断 (这几种中断时是重复中断还是抛出异常,下面的变量就是标识这两种状态)
        private static final int REINTERRUPT =  1; //重复中断
        private static final int THROW_IE    = -1; //抛异常

        // 检查节点中断状态。唤醒前中断抛异常,唤醒后中断重复中断,非中断返回0
        private int checkInterruptWhileWaiting(Node node) {            。。。        }

        //根据中断模式,在重复进入当前线程时抛异常或什么也不做
        private void reportInterruptAfterWait(int interruptMode){            。。。    }

        //进入等待队列,并释放当前锁
        public final void await() throws InterruptedException {            。。。       }
        //超时等待
        public final long awaitNanos(long nanosTimeout) throws InterruptedException {   }

        //超时等待
        public final boolean awaitUntil(Date deadline) throws InterruptedException { }

        //超时等待
        public final boolean await(long time,TimeUnit unit)throws InterruptedException {}

        //判断等待对象是否由制定的AQS创建
        final boolean isOwnedBy(AbstractQueuedSynchronizer sync) {        }

        //查询等待队列中是否还有等待节点
        protected final boolean hasWaiters() {            。。。        }

        //获取等待队列长度
        protected final int getWaitQueueLength() {            。。。        }

        //从等待队列中获取所有等待线程
        protected final Collection<Thread> getWaitingThreads() {            。。。      }
    }

可以看到ConditionObject内部类也很简单,主要是封装了等待队列的首尾节点以及唤醒和各种等待方法,因为大多方法功能相近,这里我们主要分析一下常用的等待和唤醒方法:

等待方法如下:

public final void await() throws InterruptedException {
            //1、等待状态对中断是有响应的,如果检测到中断位为true,则抛异常
            if (Thread.interrupted())
                throw new InterruptedException();
            //2、以当前线程创建节点(初始化节点状态为-2),并添加到等待队列中(这一步要注意,等待队列中节点间不设置pre、next属性,基本是只设置firstWaiter、lastWaiter、nextWaiter,所以在流程4中isOnSyncQueue方法可以根据引用情况判断节点是否在同步队列)
            Node node = addConditionWaiter();
            //3、释放锁状态,且如果释放失败则直接设置节点状态为取消态、释放成功则返回当前线程之前持有的锁状态信息
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            //4、判断当前节点是否在同步队列,如果不在同步队列,那就直接挂起
            while (!isOnSyncQueue(node)) {
                //4.1、挂起当前线程
                LockSupport.park(this);
                //4.2、如果线程在中断态且线程节点放入了同步队列,则跳出循环
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //5、流程4中节点从等待队列进入同步队列,此时执行节点的竞争锁方法进行资源的竞争获取,如果获取到锁,则根据中断类型设置中断变量
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            //6、能进入这一步说明当前节点已经放在同步队列且执行了,此时检查它之后是否还有其它等待节点,如果有则从等待队列中解除非等待态的节点。
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            //7、如果中断模式与方法中默认设置的0不同,则根据中断模式类型,执行不同的中断方法
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

唤醒方法:

public final void signal() {
            //1、判断当前线程是否持有锁,没持有则抛异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            //2、获取等待队列第一个等待节点,唤醒其进入同步队列竞争锁
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

private void doSignal(Node first) {
            do {
                //1、如果当前节点的下一个等待节点为空,说明等待队列为空,此时将lastWaiter也设置为空
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                //2、上一步中已经把nextWaiter传递给firstWaiter属性,所以此时吧该属性设空,便于后续GC
                first.nextWaiter = null;
            //3、把当前节点从等待队列转到同步队列,成功则跳出当前循环,失败则遍历等待队列节点知道有一个成功。(这里先是通过&&的截断功能保证失败时自动遍历下一个节点,另一个则是只有有一个成功就跳出循环)
            } while (!transferForSignal(first) && (first = firstWaiter) != null);
        }

final boolean transferForSignal(Node node) {
        //1、尝试将节点从等待状态修改为同步队列中的初始态,一般在等待节点被取消时CAS执行失败
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        //2、插入节点到同步队列并返回其前驱节点
        Node p = enq(node);
        int ws = p.waitStatus;
        //3、前驱节点为取消态或者设置前驱节点为-1状态失败,则直接唤醒线程,从而重新进入锁竞争的同步流程
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

上述就是简单的等待唤醒方法源码,没有过多深入讲解细节,后续用到再深入讲解,这里用文字描述一下流程:

等待:首先是判断节点是否中断,wait状态下中断会抛出异常;然后创建等待状态节点添加到等待队列尾部(这里会先过滤一边等待队列取消状态的节点,另外这一步要注意,等待队列中节点间不设置pre、next属性,基本是只设置firstWaiter、lastWaiter、nextWaiter,所以在isOnSyncQueue方法可以根据引用情况判断节点是否在同步队列);随后释放当前线程持有的是锁状态;然后再接着判断节点是否从等待队列转到了同步队列,如果没有挂起线程直至signal通知唤醒;线程被唤醒后,判断如果线程在中断态且线程节点放入了同步队列,则跳出循环;然后进入同步队列的该节点通过公平竞争锁方法获取锁并执行。

唤醒:首先是判断当前线程持有锁,否则抛异常;然后唤醒等待队列中的第一个节点,如果等待节点被取消,则遍历等待队列直至有一个节点被转移到同步队列。

无论是获取锁方法还是释放锁方法,都是通过修改属性变量来实现的,所以变量的信息很重要,先来看一下AQS的核心变量信息:

    private transient volatile Node head; // 同步队列首节点
    private transient volatile Node tail; // 同步队列尾节点
    private volatile int state; // 同步器状态,即锁的状态

到现在为止我们心里应该有个大致的轮廓了,首先是无论等待队列还是同步队列,节点都来自于Node对象,且节点内有状态变量表明当前线程节点处于的状态;其次等待队列具有firstWaiter、lastWaiter指针,唤醒后可以进入同步队列竞争锁,且等待、唤醒方法均由ConditionObject对象封装好;最后同步队列具有head、tail指针,然后竞争修改state状态,且这些变量都在AQS中,所以同步队列相关的获取锁释放锁方法也在该AQS对象本体内。下面我们来看几个常见的锁获取和释放的方法,因为AQS分共享和独占两种形式,所以这里我们从共享锁的获取释放以及排他锁的获取释放来查看同步队列中锁的使用,由于源码比较多,这里在下一篇讲,这里我们先提一下源码中几个比较关键的点,方便读取源码的时候更容易去理解:

1)一些遍历非取消态的源码中,使用从尾部节点向头节点遍历的方式。之所以不是从头向尾便利,可以参考addWaiter源码中注解

2)区分挂起和中断的区别,前者是不再执行,后者仍会继续执行

3)设置头节点为什么要把线程置空

4)共享传播是将锁传播整个同步队列还是怎么回事

5)Node节点中waitStatus各状态的含义和切换的场景(如果不清楚可以去本文前段部分查找看一下)

参考链接:

博客园 AQS基础原理 https://www.cnblogs.com/zofun/p/12206759.html

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值