zoukankan      html  css  js  c++  java
  • JDK源码那些事儿之传说中的AQS-独占锁

    上一篇文章中笔者已经介绍过AQS的基础知识,但是更加具体的源码实现还未涉及,本篇文章笔者就结合具体的AQS实现类来聊一聊AQS的独占锁实现过程

    前言

    JDK版本号:1.8.0_171

    之前的文章中已经介绍过,AQS是一套多线程访问共享资源的同步器框架实现,开发者只需要按照需求在实现类中实现对应的方法即可完成锁或者同步器的构建,在AQS的源码注释部分,如果你有认真看过的话应该会注意到,作者举了2个简单的例子,一个是独占锁实现Mutex,另一个是共享锁实现BooleanLatch

    作为示例笔者就以这两个简单的锁实现结合AQS的具体方法来进行讲解说明,这里提醒下请先看下笔者的上篇文章JDK源码那些事儿之传说中的AQS-概览,了解AQS的基本知识,同时阅读下之前对ObjectLockSupport的讲解,笔者默认读者应该已经了解了这些相关知识点

    其次,个人认为初学者学习AQS的源码需要多思考多debug,多看几遍本篇文章才能理解。由于篇幅过长,笔者这篇文章只讲解简单的独占锁实现流程

    示例

    先来看下源码作者实现的独占锁Mutex,可以看到封装了内部类Sync继承AbstractQueuedSynchronizer实现了独占锁需要的几个方法,这里主要是tryAcquiretryRelease,其他方法留给读者自行深入,这里再复习下AQS定义这两个方法的含义:

    • tryAcquire:独占模式尝试获取资源,成功则返回true,失败则返回false
    • tryRelease:独占模式尝试释放资源,成功则返回true,失败则返回false

    这里先了解下即可,可略过继续看下面的测试代码实现

    class Mutex implements Lock, Serializable {
    
        // Our internal helper class
        private static class Sync extends AbstractQueuedSynchronizer {
            // Reports whether in locked state
            @Override
            protected boolean isHeldExclusively() {
                return getState() == 1;
            }
    
            // Acquires the lock if state is zero
            @Override
            public boolean tryAcquire(int acquires) {
                assert acquires == 1; // Otherwise unused
                if (compareAndSetState(0, 1)) {
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
                return false;
            }
    
            // Releases the lock by setting state to zero
            @Override
            protected boolean tryRelease(int releases) {
                assert releases == 1; // Otherwise unused
                if (getState() == 0)
                    throw new IllegalMonitorStateException();
                setExclusiveOwnerThread(null);
                setState(0);
                return true;
            }
    
            // Provides a Condition
            Condition newCondition() {
                return new ConditionObject();
            }
    
            // Deserializes properly
            private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
                s.defaultReadObject();
                setState(0); // reset to unlocked state
            }
        }
    
        // The sync object does all the hard work. We just forward to it.
        private final Sync sync = new Sync();
    
        @Override
        public void lock() {
            sync.acquire(1);
        }
    
        @Override
        public boolean tryLock() {
            return sync.tryAcquire(1);
        }
    
        @Override
        public void unlock() {
            sync.release(1);
        }
    
        @Override
        public Condition newCondition() {
            return sync.newCondition();
        }
    
        public boolean isLocked() {
            return sync.isHeldExclusively();
        }
    
        public boolean hasQueuedThreads() {
            return sync.hasQueuedThreads();
        }
    
        @Override
        public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }
    
        @Override
        public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
            return sync.tryAcquireNanos(1, unit.toNanos(timeout));
        }
    }
    

    笔者这里写了一个简单的使用示例供大家参考,其中使用了CountDownLatch,仅仅是保证线程池中的所有线程同一时刻争抢Mutex独占锁,读者可以不用过多关注,把这个去掉也可以,不是重点,我们重点关注下Mutex的使用即可,最终的结果想必读者也能猜到,其中的线程一个一个争抢到锁才能执行业务逻辑操作

        int threadNum = 10;
        CountDownLatch countDownLatch = new CountDownLatch(1);
        Mutex mutex = new Mutex();
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        Runnable task = () -> {
            try {
                // 保证同时开始争抢锁
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 获取锁,未获取则阻塞等待
            mutex.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "获取锁");
                try {
                    // 模拟业务处理
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                System.out.println(Thread.currentThread().getName() + "释放锁");
                mutex.unlock();
            }
        };
        // 开启10个线程争抢互斥锁
        for (int i = 0; i < threadNum; i++) {
            threadPool.submit(task);
        }
    
        // 同时开始执行
        countDownLatch.countDown();
    
        threadPool.shutdown();
    

    核心代码

    在上面的使用示例里最重要的部分在于下面这段:

        // 获取锁,未获取则阻塞等待    
        mutex.lock();
        try {
    
        } finally {
            // 释放锁
            mutex.unlock();
        }
    

    笔者就结合这个示例来分析AQS独占锁底层源码实现的流程,为了方便讲解,先规定下场景:

    线程池中包含3个线程,最终线程获取到独占锁执行业务逻辑的顺序为:线程A->线程B->线程C(注意,这里顺序是随机的,笔者只是为了方便讲解这样设置)

    那么AQS底层到底是如何流转的呢?如何在Java语言层面上保证独占呢?释放锁时如何唤醒等待的线程呢?可以先思考几分钟,想想如果让你来设计,你会怎么做?

    lock

    看下调用过程

    mutex.lock() -> sync.acquire(1) -> !tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

    mutex.lock()在内部类Sync的lock方法上,而这个方法最终是在调用AbstractQueuedSynchronizeracquire方法,这个方法含义如下:

    以独占模式获取共享资源,忽略中断,即线程在aquire过程中,中断此线程是无效的,同时至少调用一次tryAcquire方法,成功获取到共享资源则返回,否则,将当前想要独占资源的线程封装成Node节点添加到同步队列(sync queue)中,阻塞等待获取共享资源

    初学者可能会感到困惑,这里tryAcquire就是在尝试获取共享资源,成功则不需要进行第二个条件了,否则就需要将当前线程封装成节点添加到队列中阻塞等待,结合上面示例,你也应该明白,为什么线程B和线程C在线程A未执行完毕时一直阻塞等待了吧,那么这里具体是怎么实现的呢?笔者带领大家一步一步往下看

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

    查看源码tryAcquire的实现,这个方法就是子类需要实现的了,可能Java新手会觉得有点绕,最好先去了解下模板方法设计模式的使用,会相对轻松些,在上面的示例中内部类实现如下:

        // Acquires the lock if state is zero
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // Otherwise unused
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
    

    非常简单明了,就是通过CAS尝试将state从0设置为1,你也能看出来,这里入参是为了通用性而传入的值(对于这个示例来说无意义),这里重点理解的部分在于如果CAS成功了,即将state从0设置成了1,表明当前线程获取到了共享资源,对于独占锁来说,就是当前线程独占,通过setExclusiveOwnerThread记录独占线程,返回true表明成功,最终在示例代码中也就是mutex.lock处不会进行阻塞,会继续向下执行程序,此时线程A获取到锁时AQS的情况如下:


    获取AQS

    继续执行,在线程A还未通过mutex.unlock()释放锁时,线程B执行mutex.lock()获取锁,线程B在这里会阻塞住等待锁的释放,获取到锁才会继续执行,那么底层到底是如何实现的呢?

    再回过头看下tryAcquire,这里返回false,再看acquire,执行第二个条件acquireQueued(addWaiter(Node.EXCLUSIVE), arg),这里涉及到了两个方法,一个一个来看底层是如何处理的

    首先是addWaiter(Node.EXCLUSIVE)方法,去AQS看下实现:

        private Node addWaiter(Node mode) {
            // 以给定的模式创建当前线程的节点
            Node node = new Node(Thread.currentThread(), mode);
            // 添加到同步队列尾部
            Node pred = tail;
            // 尾节点非空,即已经被初始化了
            if (pred != null) {
                // node前驱指向pred
                node.prev = pred;
                // CAS更新尾节点为新增节点node
                if (compareAndSetTail(pred, node)) {
                    pred.next = node;
                    return node;
                }
            }
            // 尾节点为空或CAS更新尾节点失败则调用enq来操作直到成功
            enq(node);
            return node;
        }
    

    这里只针对示例中的独占锁实现分析,你可以看到源码中Node.EXCLUSIVE = null,AQS的head,tail,state在Sync内部类中已经初始化过了,初始值为null,null和0,继续看源码做了什么,首先创建了一个Node节点,调用如下,这里可以看到设置了节点的this.nextWaiter = mode = Node.EXCLUSIVE,thread保存当前线程

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
    

    线程B执行时,pred = null故这里直接进入enq(node)方法,看下源码实现:

        private Node enq(final Node node) {
            // 无限循环保证插入节点中
            for (;;) {
                // 获取尾节点
                Node t = tail;
                // 尾节点为空,先初始化,CAS设置头节点
                // 成功则设置尾节点
                if (t == null) { // Must initialize
                    // 头节点为空,设置头节点为新创建的节点
                    if (compareAndSetHead(new Node()))
                        // 头尾节点指向同一个新节点
                        tail = head;
                } else {
                    // 设置node节点前驱为尾节点
                    node.prev = t;
                    // CAS更新尾节点为node节点
                    // 成功则更新旧的尾节点的next指向node
                    // 链表关系才算更新完毕,返回旧的尾节点
                    if (compareAndSetTail(t, node)) {
                        t.next = node;
                        return t;
                    }
                }
            }
        }
    

    综上所述,线程B执行流程如下:

    1. 首次循环尾节点为空
    2. 创建一个空Node节点
    3. 头尾都指向这个节点
    4. 第二次循环尾节点非空
    5. 将参数的节点node添加到链表中并更新tail指向
    6. 返回node的前一个节点,这里也就是head指向的空Node节点

    此时内部结构图示如下:


    B执行1

    接下来再回头看下acquireQueued(addWaiter(Node.EXCLUSIVE), arg),看下源码中是如何处理的

    • 参数一为线程B封装成的Node节点
    • 参数二为传入的int值1
        final boolean acquireQueued(final Node node, int arg) {
            boolean failed = true;
            try {
                // 中断标识
                boolean interrupted = false;
                for (;;) {
                    // 获取node的前驱节点
                    final Node p = node.predecessor();
                    // 前驱节点为头节点,同时获取共享资源成功
                    if (p == head && tryAcquire(arg)) {
                        // 设置头节点为node节点
                        setHead(node);
                        // 清理p节点
                        p.next = null; // help GC
                        failed = false;
                        return interrupted;
                    }
                    // 前驱非头节点或获取共享资源失败则检查并挂起线程
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        interrupted = true;
                }
            } finally {
                // 失败取消节点继续获取资源
                if (failed)
                    cancelAcquire(node);
            }
        }
    

    线程B执行流程如下:

    1. 获取到线程B封装成的Node节点的前一个节点,这里也就是AQS的head节点
    2. 通过tryAcquire(arg)尝试获取锁,因为线程A还未释放,所以第一个if条件失败,
    3. 进入第二个if条件

    先看下shouldParkAfterFailedAcquire(p, node),从这个方法名你应该稍微能明白该方法的作用,获取锁失败是否应该阻塞线程,首先明确下传参:

    • 参数一p是head节点
    • 参数二node是线程B封装的Node节点
        private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
            // 前驱节点状态
            int ws = pred.waitStatus;
            if (ws == Node.SIGNAL)
                // pred节点已经设置为SIGNAL直接返回,可进行park操作
                return true;
            if (ws > 0) {
                // 前驱节点pred处于CANCELLED状态
                // 找到其前驱节点中非CANCELLED状态的节点,
                // node的前驱指向这个新的pred节点
                do {
                    node.prev = pred = pred.prev;
                } while (pred.waitStatus > 0);
                // 更新pred节点next指针指向node
                pred.next = node;
            } else {
                // 更新为SIGNAL
                compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
            }
            // 不能进行park操作
            return false;
        }
    

    线程B执行流程如下:

    1. 第一次执行shouldParkAfterFailedAcquire(p, node),head节点的waitStatus默认为0,compareAndSetWaitStatus(pred, ws, Node.SIGNAL)更新head节点的waitStatus为SIGNAL(不清楚Node状态的含义可以去看下上一篇文章,表示后继节点中的线程在等待当前节点唤醒,这里后继节点也就是node,也就是线程B封装的Node节点)
    2. 返回false,shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()这次执行就算结束了,在acquireQueued里继续循环
    3. 第二次执行shouldParkAfterFailedAcquire(p, node),此时head节点的waitStatus为SIGNAL,进入第一个if (ws == Node.SIGNAL)条件中,返回true
    4. acquireQueued条件里开始执行第二个方法parkAndCheckInterrupt(),进行阻塞线程操作(在唤醒时检查线程中断状态)

    看下parkAndCheckInterrupt源码实现:

        private final boolean parkAndCheckInterrupt() {
            // 设置当前对象为blocker
            LockSupport.park(this);
            // 返回中断标记
            return Thread.interrupted();
        }
    

    这里再提醒下,没了解LockSupport的读者麻烦先去看下笔者之前对LockSupport文章的讲解,或者先去Google了解下,AQS通过LockSupport.park(this)来对当前线程进行阻塞,到这里线程B将一直阻塞直到被唤醒,后面释放锁的线程唤醒阻塞线程时笔者会继续从这里再继续进行说明,此时AQS中状态如下:


    B执行2

    线程B阻塞等待获取锁的整个方法执行流程,读者可参考下图进行理解:


    B执行流程

    同样的,此时线程C执行mutex.lock()获取锁时,读者可以类比上面线程B执行的过程,梳理下线程C的过程,线程C同样获取不到锁,执行流程如下:

    1. tryAcquire返回false
    2. addWaiter将当前线程封装成Node节点,更新tail指针,此时tail指向线程C对应的Node
    3. 执行acquireQueued方法,前一个节点非head节点,直接执行shouldParkAfterFailedAcquire
    4. 更新前驱节点也就是线程B对应的节点状态为SIGNAL,acquireQueued第一次循环执行结束
    5. 开始第二次循环操作,进入shouldParkAfterFailedAcquire,返回true,开始执行parkAndCheckInterrupt,通过LockSupport.park阻塞等待

    此时同步队列如下图:


    队列

    unlock

    看下释放锁操作,在线程A获取锁之后执行完业务逻辑执行mutex.unlock(),看下调用过程:

    mutex.unlock() -> sync.release(1) -> tryRelease(arg) -> unparkSuccessor(h)

    代码实现如下:

        public void unlock() {
            sync.release(1);
        }
    

    然后就到AQS的方法了

        public final boolean release(int arg) {
            // 尝试释放,子类需要实现
            if (tryRelease(arg)) {
                Node h = head;
                // 头节点非空且头节点状态非0
                if (h != null && h.waitStatus != 0)
                    // 唤醒头节点后继节点
                    unparkSuccessor(h);
                return true;
            }
            return false;
        }
    

    子类实现如下:

        protected boolean tryRelease(int releases) {
            assert releases == 1; // Otherwise unused
            if (getState() == 0)
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
    

    此时线程A执行流程如下:

    1. tryRelease(1)尝试释放,进入Sync类实现的tryRelease方法
    2. 通过setExclusiveOwnerThread将独占线程属性设置为null
    3. setState将state设置为0,返回true
    4. head节点非null同时waitStatus非0时通过unparkSuccessor唤醒线程B继续执行

    注意,tryRelease只是更新了AQS的相关属性,还未进行阻塞线程的唤醒操作,看下代码逻辑,head节点非null同时waitStatus非0,这里也就是上面线程B尝试获取锁失败后更新了前一个节点也就是head节点的waitStatus的原因,通过head节点的waitStatus来判断是否需要唤醒后继节点

    unparkSuccessor入参在线程A执行过程中为head节点

        private void unparkSuccessor(Node node) {
    
            int ws = node.waitStatus;
            // 节点状态处于SIGNAL(-1)/CONDITION(-2)/PROPAGATE(-3)时CAS更新状态为0
            // 唤醒后继节点,尝试将当前节点置为初始状态
            if (ws < 0)
                compareAndSetWaitStatus(node, ws, 0);
    
            // 获取后继节点
            Node s = node.next;
            // 节点为null或者处于CANCELLED(1)时
            // 从尾节点从后向前进行遍历
            if (s == null || s.waitStatus > 0) {
                s = null;
                for (Node t = tail; t != null && t != node; t = t.prev)
                    // 找到有效节点,非CANCELLED(1)节点
                    if (t.waitStatus <= 0)
                        s = t;
            }
            // 非null唤醒节点s的线程
            if (s != null)
                LockSupport.unpark(s.thread);
        }
    

    来看下线程A释放锁的执行流程:

    1. head的节点waitStatus为SIGNAL,这里就是ws,执行compareAndSetWaitStatus将head节点的waitStatus置为0
    2. 获取head节点的后继节点,也就是线程B封装成的Node节点
    3. 判断,第一个条件后继节点为null或者节点处于CANCELLED状态(这个状态也就是取消继续获取),说明节点s非有效节点
    4. 从tail节点向前查找第一个有效节点t,s指向t,这里B线程对应节点是有效节点,不需要执行
    5. s非null,LockSupport.unpark唤醒s节点线程,这里也就是唤醒线程B

    此时线程A已经执行完毕,将锁释放,唤醒线程B继续执行,线程B阻塞等待在parkAndCheckInterrupt方法中,上面已经进行了说明,再次看下这个方法

        final boolean acquireQueued(final Node node, int arg) {
            boolean failed = true;
            try {
                // 中断标识
                boolean interrupted = false;
                for (;;) {
                    // 获取node的前驱节点
                    final Node p = node.predecessor();
                    // 前驱节点为头节点,同时获取共享资源成功
                    if (p == head && tryAcquire(arg)) {
                        // 设置头节点为node节点
                        setHead(node);
                        // 清理p节点
                        p.next = null; // help GC
                        failed = false;
                        return interrupted;
                    }
                    // 前驱非头节点或获取共享资源失败则检查并挂起线程
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        interrupted = true;
                }
            } finally {
                // 失败取消节点继续获取资源
                if (failed)
                    cancelAcquire(node);
            }
        }
    

    线程B被唤醒之后执行流程如下:

    1. 继续循环,线程B对应节点的前驱节点即head节点,tryAcquire获取到共享资源(锁)成功
    2. 设置头节点为线程B对应的节点,这里新设置了head节点,注意下
    3. 清理原head节点
    4. 返回是否中断标志,若失败则取消节点继续获取资源

    此时AQS图示如下:


    唤醒B

    同样的,当线程B执行mutex.unlock()释放锁时,读者可以类比上面线程A执行的过程,梳理下线程B释放锁,线程C被唤醒的过程,执行流程如下:

    1. tryRelease尝试释放锁,进入Sync类实现的tryRelease方法,独占线程属性设置为null,更新state为0
    2. head节点非null同时waitStatus非0时执行unparkSuccessor(这里head节点在上面线程B被唤醒后已经替换成线程B对应的节点了)
    3. head的节点waitStatus为SIGNAL,这里就是ws,执行compareAndSetWaitStatus将head节点(线程B对应的)的waitStatus置为0
    4. 获取head节点的后继节点,也就是线程C封装成的Node节点
    5. 判断这里线程C封装成的Node节点是有效节点
    6. 非null,LockSupport.unpark唤醒节点对应的线程,这里也就是唤醒线程C

    好,到这里继续重复上面的流程,也就是C被唤醒之后获取到锁然后释放的过程,释放过程稍有不同,因为没有后继节点线程需要被唤醒了,这里线程C释放锁的流程也梳理下:

    1. tryRelease尝试释放锁,进入Sync类实现的tryRelease方法,独占线程属性设置为null,更新state为0
    2. head节点(线程C对应的节点)非null但是waitStatus=0,不需要执行unparkSuccessor操作,因为没有后继节点线程需要被唤醒,直接返回即可

    总结

    至此,通过源码作者的独占锁示例,结合简单使用示例,笔者通过图示和核心源码执行过程进行了详细讲解说明,独占锁实现流程层层递进,相信读者应该足够了解AQS了

    当然,限于篇幅,笔者未对AQS的所有方法进行说明,希望有兴趣的同学自行查阅了解,希望对各位有所帮助

    以上内容如有问题欢迎指出,笔者验证后将及时修正,谢谢

    作者:freeorange
    个人博客网站:https://www.gclearning.cn/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    Odoo安装教程2-创建新的插件模块第一讲
    Odoo安装教程1-创建第一个 Odoo 应用
    Odoo开发教程21-Odoo服务器端开发者模式
    Ubuntu 安装LAMP
    Roundcube Webmail信息泄露漏洞(CVE-2015-5383)
    Roundcube Webmail跨站脚本漏洞(CVE-2015-5381 )
    Roundcube Webmail File Disclosure Vulnerability(CVE-2017-16651)
    Roundcube 1.2.2
    XAMPP重置MySQL密码
    python importlib动态导入模块
  • 原文地址:https://www.cnblogs.com/freeorange/p/13869374.html
Copyright © 2011-2022 走看看