并发系列(八)-----AQS详解独占模式资源的获取与释放

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/MyPersonalSong/article/details/84135710

一 简介

上一篇总结了AQS的整体架构,以及它的组成,和它们之间的关系。AQS主要的三部分分别是volatile修饰的变量、同步队列和等待队列其中,同步队列在上篇总结中已经介绍过了,不知道的话可以点这里AQS的框架组成以及同步队列源码解析。这一片文章主要总结独占模式下资源的获取。

二 资源的获取源码解析

在上一篇总结中,最后过源码的时候看到addWater(),同时我们也提出两个猜想获取资源的两种方式

猜想一:线程上来就直接获取,如果获取成功的话那就执行了,获取失败的话被封装成节点添加到同步队列中

猜想二:线程一上来看看队列中有没有要同步的节点,如果有的话那就不获取资源了直接添加到同步队列中,等待上一个节点唤醒。

现在看一下操作stste的方法有哪些


    /**
     * 返回当前同步状态
     */
    protected final int getState() {
        return state;
    }

    /**
     * 设置当前的同步状态
     */
    protected final void setState(int newState) {
        state = newState;
    }

    /**
     * 使用CAS来更新state的值
     *
     * @param except 期望值
     * @param update 更新值
     * @return 更新是否成功
     */
    protected final boolean compareAndSetState(int except, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, except, update);
    }

AQS下面这个方法就是来获取资源state的

    /**
     * 以独占模式获取,忽略中断。实现 至少调用一次{@link #tryAcquire},
     * 成功回归。 否则,线程可能会排队      
     *
     * @param arg 资源请求
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
            selfInterrupt();
        }
    }

看到上面的代码我们看到了比较熟悉的方法就是addWaier(),这个方法返回一个封装好线程的节点,被当作参数传递到acquireQueued()这个方法中。但是acquireQueued执行不执行取决与前面的tryAcquire()这个方法。当tryAcquire()返回false的时候才会去执行acquireQueued()这个方法。再看一下tryAcquire()这个方法。

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

尽然抛出一个异常,在第一遍的文章说过AQS是基于模板方法的框架,既然使用的是模板方法那就需要子类去实现了,在ReentrantLock内部类Sync中是AQS的实现。下面是源码

  @Override
        protected final boolean tryAcquire(int acquire) {
            return nonfairTryAcquire(acquire);
        }

 final boolean nonfairTryAcquire(int acquires) {
            //获取但当前线程
            final Thread current = Thread.currentThread();
            //获取资源的状态
            int c = getState();
            if (c == 0) {
                //如果资源的状态为0的话说明state是没有线程持有当前资源的
                if (compareAndSetState(0, acquires)) {
                    //使用CAS替换,如果成功那就将独占线程设为当前线程,也就意味着当前线程
                    //拥有执行时间了
                    setExclusiveOwnerThread(current);
                     //获取的资源返回true
                    return true;
                }
            } else if (current == getExclusiveOwnerThread()) {
                //如果state不为0的话独占线程是当前线程的话那么给state加一,这里是
                //重入锁的实现
                int nextc = c + acquires;
                if (nextc < 0) {
                    throw new Error("Maximum lock count exceeded");
                }
                setState(nextc);
                return true;
            }
            //如果没获取到资源的话返回false
            return false;
        }

好了tryAcquire()方法干什么的已经知道了,如果获取到资源的话返回true,没有获取到资源的话就返回false,现在再看acquire()这个方法,当线程获取到资源的时候返回的是true ,!true也就是false,那就不必在在执行&&面的语句了,如果没有获取到资源,就要执行后面的语句了,首先将当前线程包装成一个节点添加到对列尾部并返回这个节点。既然包装了,那就要处理将这个节点了。acquireQueued方法就是干这个的,下面是源码

 /**
     * 将竞争节点设置为头节点,同时当前节点不是头节点的话
     *
     * @param node 要获取头节点的节点
     * @param arg  state状态参数
     * @return 返回true表示线程发生了中断
     */
    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.next = null;
                    failed = false;
                    return interrupted;
                }
                //这里是for循环的移动条件跳过前驱接节点未取消状态的节点,
                //当前线程中断的话只能返回去等待了
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt()) {
                    interrupted = true;
                }
            }
        } finally {
            //当线程中断直接取消当前节点竞争
            if (failed) {
                cancelAcquire(node);
            }
        }

    }

对于上面一段源码来说,获取资源是不难理解的,但是没有获取到资源时候执行了一个if语句,看一下if语句中两个方法中分别做了什么。下面是源码


    /**
     * 检查是否可以在节点后添加竞争节点,同时检查node前驱节点是否取消,如果取消了就要将这个节点
     * 移除掉,如果在前驱节点等待中返回true
     *
     * @param pred 前驱节点
     * @param node 当前节点
     * @return 返回boolean
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //获取前驱节点的状态
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL) {
            return true;
        }
        if (ws > 0) {
            do {
                //如果前驱节点的的状态为取消状态那么跳过直到找到可以
                //获取竞争的node
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

当node中的前驱节点是等待中的时候就会执行下一个方法,下一个方法的源码如下

    /**
     * 将线程至于waiting中 并返回线程是否中断
     *
     * @return 中断线程的boolean
     */
    private final boolean parkAndCheckInterrupt() {
        //将线程至于waiting中
        LockSupport.park(this);
        return Thread.interrupted();
    }

看到这里基本上已经清楚了,acquireQueued()方法就是如果可以获取到资源的时候直接获取,不能获取到资源时检查父节点是否在等待状态中,如果在等待中,就调用LockSupport.park(),将当前线程至于等待状态,等待中断或着唤醒。AQS获取资源也就完了。

AQS获取资源的总结:

1.首先先使用CAS获取支援state。如果获取成功的话就不在添加节点了,如果获取失败的话将当前线程封装到node中。

2.在添加节点的时候判断前驱节点是否是头节点,如果前驱节点是头节点的话,继续for循环获取资源。如果不是头节点的话检查当前的前驱节点是否为空,将当前节点的所有前面的节点为取消状态的全都去掉。去掉之后当前节点还不是头节点时,将线程至于waiting状态等待中断或唤醒。

上面的总结也就验证了的猜想一,其实就是非公平锁的实现,猜想二是公平的锁的实现。可以在ReentrantLock中的内部类FairSync看到公平锁的实现。

三 资源的释放

关于支援的释放我认为是比较简单的,可以大体的猜想一下,找到持有资源的node节点,将state的值设置为0,再将当前node节点从同步队列中移除掉。然后唤醒下一个节点的线程。下面是源码

    /**
     * 释放资源,并唤醒下一个节点
     *
     * @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;
    }

上面源码干什么已经在注释中说的很清楚了,但是没有看到资源是如何操作的。其中资源的操作就在tryRelease()方法中下面是源码。

    /**
     * 线程调用释放锁
     *
     * @param arg 状态
     * @return 是否释放成功
     */
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

这个方法和tryAcquire()方法一样都是抛了一个异常我们看其中子类的实现。下面是源码。

    /**
         * 释放资源
         *
         * @param release 释放锁的数字
         * @return 是否释放成功
         */
        @Override
        protected final boolean tryRelease(int release) {
            //获取到资源并减去资源
            int c = getState() - release;
            
            if (Thread.currentThread() != getExclusiveOwnerThread()) {
                throw new IllegalMonitorStateException();
            }
            boolean free = false;
            if (c == 0) {
                free = true;
                //当state为0的时候表示资源释放完成想独占的线程设置为null
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

四 总结

 从上面的源码中我们可以看出,锁的实现无非就是资源的获取,与队列的操作,线程状态的转化。

获取资源时:先操作state,操作成功就直接获取到了,操作不成功添加到同步队列中,调用LockSuppport.park(),将线程至于waiting状态等待中断或唤醒。

资源释放:先操作state,操作成功的话将队列中的当前节点移除,唤醒下一个节点。

猜你喜欢

转载自blog.csdn.net/MyPersonalSong/article/details/84135710
今日推荐