• AQS源码分析笔记


    经过昨晚的培训.对AQS源码的理解有所加强,现在写个小笔记记录一下

    同样,还是先写个测试代码,debug走一遍流程, 然后再总结一番即可.

     

    测试代码

    import java.util.concurrent.CyclicBarrier;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class AqsTest {
        public static void main(String[] args) {
            Lock lock = new ReentrantLock();
            // 创建一个CyclicBarrier,以便于让50个线程同时去抢占锁资源
            CyclicBarrier cyclicBarrier = new CyclicBarrier(50);
            for (int i = 1; i <= 50; i++) {
                new Thread(() -> {
                    try {
                        cyclicBarrier.await();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "开始抢锁");
                    lock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() + "抢锁成功");
                        Thread.currentThread().sleep(1000);
                        System.out.println(Thread.currentThread().getName() + "释放锁");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                    }
                },"线程"+i).start();
    
            }
            System.out.println("main over");
        }
    }

     

    下面开始流程分析

    1. 创建锁对象

    Lock lock = new ReentrantLock();

    本示例使用了重入锁,底层是非公平同步锁NonfairSync类, 先看一下类图关系

    很明显,NonfairSync顶层类正是本篇的主角AQS

    查看AQS源码,它有四种重要的属性需要我们去关注

    private transient volatile Node head;
    private transient volatile Node tail;
    private volatile int state;
    private transient Thread exclusiveOwnerThread;  #其实它并不在AbstractQueuedSynchronizer类中,父类属性

     

    所以, 当我们在程序中new出一个Lock实例后.该Lock实例就自带这4个属性了,其中state初始值为0 ,余者皆是null. 而所谓的抢占锁资源,就是围绕着这4个属性展开. 下面我来阐述一下这四个属性意义

    (1) head , CLH链表的头节点, 懒加载, 除了第一次加载之外,只能通过setHead方法去修改head节点. 注意: 如果CLH链表头节点存在,那么必须保证头节点的 waitStatus 值不能是1(CANCELLED), 即取消状态.

    (2)tail , CLH链头的尾节点, 懒加载, 只能通过enq方法添加一个新wait节点去修改它.

    (3)state,  用它来表示同步器的状态

    (4)exclusiveOwnerThread , 独占模式下持有资源(AQS锁)的线程

     

    好,至此,创建一个Lock实例及关联的信息应该是讲清楚了, 下面我们重点分析线程是如何抢占锁资源的,也即是分析一下lock方法到底干的啥?

     

    2.抢占锁资源以及wait线程

    线程如何抢占资源? 没抢到资源的线程又咋办呢?

    下面以debug的方式来跟踪一下AQS的骚操作.

     

    还有一个需要注意的地方是,这50个线程,现在都running状态,这些状态后面是会变化.......

    先看下面截图

     

    (1) 首个抢到资源的线程

    为了便于记忆, 此处我选择让10号线程最先获取到锁资源, 具体逻辑如下.

    去掉一些渣渣代码,程序会进入到下面这个方法

    final void lock() {
        // cas修改AQS的state状态值, 如果是0,就改成1.
        if (compareAndSetState(0, 1))  // cas修改state状态值成功
            // 将exclusiveOwnerThread 设置为当前线程
            setExclusiveOwnerThread(Thread.currentThread());
        else // cas修改状态值失败
            acquire(1);  // 这里面的逻辑后面分析
    }

    说明: 

    (1)因为10号线程是第一个进来抢占资源的,此时,AQS四大属性中的state等于,所以这里cas修改值会成功,然后就会进行if操作,同时exclusiveOwnerThread的值修改成当前线程,即10号线程. 这也就表示10号线程抢到了锁资源.

    (2) 在该方法中,AQS的4大属性中有2个属性发生了变化, 此时AQS4大属性中,state等于1,exclusiveOwnerThread是10号线程, head 和 tail仍然为null

    (3) 请看下面的debug截图,证明以上分析的正确性

     

     

    以上3张截图应该能证明,10号线程抢占锁资源成功!

    此外,再补充一下上面提到的cas操作,即 compareAndSetState(0, 1)方法, 我们先看一下它的实现 

      protected final boolean compareAndSetState(int expect, int update) {
            // See below for intrinsics setup to support this
            return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
        }

    这儿使用到了unsafe类,咱也就到底为止了, 但是方法里面的几个参数还是有必要弄懂的.

    this : AQS

    expect : 期望值 

    update : 更新后的值

    stateOffset : 这就比较骚包了,我们直接看一下源码, 也使用到了unsafe类,然后就全靠自己意会了

        stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state"));

     

    好,到这儿, 10号线程抢占到了锁资源,而且仍然持有锁的, 接下来,20号线程也开始抢锁了.  (不要记了10号线程还拿着锁睡觉呢)

     

    (2) 20号线程抢占锁资源

    20号线程同样会进入到lock()方法,因为10号线程cas成功之后,AQS四大属性之一的state等于1.所以,此时20号线程进来cas会失败,然后进入else代码,

    即进入acquire(1)这个方法.

     

    请看下面的截图

     

    分析完全正确.

    然后,我们来看看accquire()方法里面干啥了......

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

    哈哈,底层就是这3个飘红的方法. 

    咱们先来看看最简单的tryAccquire方法.注意,这儿会根据实例化锁的类型不同而逻辑不同, 咱们的测试代码使用的是非公平锁,所以看NonfairSync类中的实现即可. 源码如下

    final boolean nonfairTryAcquire(int acquires) {
        // 获取当前抢占锁资源的线程
        final Thread current = Thread.currentThread();
        // 获取AQS的state的值
        int c = getState();
        if (c == 0) {
             // 如查state的值等于,使用cas将state值改成acquires
            if (compareAndSetState(0, acquires)) {
                // 将AQS的 exclusiveOwnerThread 改成当前线程
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 如果当前抢占锁资源的线程与AQS中的exclusiveOwnerThread是同一个线程
        else if (current == getExclusiveOwnerThread()) {
            // 将AQS的state值加上acquires
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            // 修改AQS的state属性值
            setState(nextc);
            return true;
        }
        return false;
    }

    这儿逻辑很简单,咱就不debug截图了, 该方法最后肯定返回false.

     

    由于tryAccquire方法返回false,所以20号线程会进入后面的逻辑, 接着看addWaiter方法, 源码如下

    private Node addWaiter(Node mode) {
        // 以排它或者说是独占的模式创建一个node节点
        Node node = new Node(Thread.currentThread(), mode);
        // 将AQS四大属性中的tail赋值给一个临时变量pred
        Node pred = tail;
        
        // 如果这个pred不为null
        if (pred != null) {   // if里面的逻辑就是创建的node节点与原CLH的tail节点互相挂载的过程
            node.prev = pred;   // 将pred节点挂到新创建的node节点的前一个节点上
            if (compareAndSetTail(pred, node)) { // cas将新创建的node节点设置成CLH链表的尾节点,即tail
                pred.next = node; // 将新创建的node节点挂到pred节点的下一个节点上
                return node; // 返回新创建的node节点
            }
        }
        // 如果pred节点等于null.就走enq方法, 由于20号线程进来就会走enq方法,所以以后我们重点分析它的执行逻辑
        enq(node);  
        return node;  // 返回新创建的node节点
    }

     

    private Node enq(final Node node) {
        for (;;) {
            // CLH链表的尾节点
            Node t = tail;
            if (t == null) { //初始化, 这也就是前面提到的懒加载问题
                if (compareAndSetHead(new Node()))  // 创建一个空的node节点,并将这个新创建的node节点设置成CLH链表的头节点
                    tail = head; // 将头节点赋值到尾节点,即,CLH中就只有一个空node
            } else {
                // 将CLH链表中的尾节点挂载到当前node节点的前一个节点上,即是当前节点成了CLH中尾节点 
                node.prev = t;  // 挂前
                if (compareAndSetTail(t, node)) {  // 挂后
                    t.next = node;
                    return t;// 返回t节点,也是这个for循环的出口,注意呀,这个t节点就是当前传入节点的前一个节点,很关键呀~~~~~~~~~~~~
                }
            }
        }
    }

     

    大家可以看到这个enq方法中有个无限循环,看到这种代码,一般都比较蛋疼, 而且在最前面,咱们也提到了使用enq方法时,会通过懒加载的方法去修改CLH链表的尾节点, 即然这么种重要,咱们有必须debug看看.

     

     

     

     

     

    enq方法的一切都尽在截图与源码注释中. 最后再补充中一点, enq方法最后的 return t 并没有什么用 , 除了跳出for循环之外.

    addWaiter方法最后返回的node仍然是包裹着抢占锁资源的线程的那个node节点, 唯一不同的是,这个node节点的pred位置已经挂载了一个空node.

    还是截个图用来验明证身.

     

    到此为止,addWaiter方法也说完了,接下该是acquireQueued方法了. 先看源码

     

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
            
                // 当前节点的前一个节点p
                final Node p = node.predecessor();
                
                // 如果p是head节点,就让当前线程去尝试获取锁资源
                if (p == head && tryAcquire(arg)) {  // 当前线程获取锁资源成功
                    // 将当前节点node设置成头节点(印证了前面说到的只有通过setHead方法来修改CLH链表的头信息)
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 如果p节点不是头节点, 通过本方法中的for循环调用shouldParkAfterFailedAcquire修改p节点的waitStatus值
                if (shouldParkAfterFailedAcquire(p, node) && 
                    parkAndCheckInterrupt())  // 如果p节点的waitStatus值等于SIGNAL, 就是当前线程安全挂起  LockSupport.park(this);
                    interrupted = true;
            }
        } finally {
            if (failed) 
                // 如果p是头节点,而且当前线程获取锁资源成功,在这儿就取消当前节点获取锁资源, 合理
                cancelAcquire(node);
        }
    }

     

     

    /**
     * 
     *  检测和更新accquire资源失败的节点的状态. 如果当前node节点(thread)应该被阻塞,返回true.
     *  该方法在循环抢占资源的过程中,主要控制信号量signal
     *
     * @param pred  当前节点node的前一个节点
     * @param node  当前节点
     * @return {@code true} 如果当前node节点中的线程应该被挂起,返回true
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 前一个节点的waitStatus值
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * 当前node节点可以释放状态.表示可以安全的挂起
             */
            return true;  // 返回true, 当前node节点(thread)会被挂起park,同时也会跳出acquireQueued中for循环
        if (ws > 0) {
            /*
             * 如果pred节点是取消状态, 那么就将pred节点的前一个节点挂载到当前node节点的前一个节点上.
             * 循环调用,移除CLH链上所有设置为取消状态的节点
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            // 将node挂载到pred节点的下一个节点上. (因为CLH中的节点是双向链表,所以需要互相挂载)
            pred.next = node;
        } else {
            /*
             * 将pred节点的waitState设置成signal
             * 为啥需要这些样呢?   
             * 配合着acquireQueued方法就不难理解,只能将状态设置成signal, 当前节点node(Thread)才会被挂起park,
             * 这时,你会看到线程由Running状态变成了WAIT状态
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;  // 返回false时,继续执行acquireQueued方法中的死循环
    }

     

        private final boolean parkAndCheckInterrupt() {
            LockSupport.park(this);
            return Thread.interrupted();
        }

     

        public static void park(Object blocker) {
            Thread t = Thread.currentThread();  // 看到没有,当前线程
            setBlocker(t, blocker);
            UNSAFE.park(false, 0L);
            setBlocker(t, null);
        }

     

    由于上面这几个方法算是lock加锁过程中核心方法,所以,我们debug截图记录

     

     

    20号线程第一次抢占锁资源失败.

     

    代码继续往下执行,就会进入到 shouldParkAfterFailedAcquire(p,node)方法中. 请注意呀,本次p就是head节点,而node节点中的thread就是20号线程.

    由于shouldParkAfterFailedAcquire方法内部逻辑比较简单,咱就不截图了. 

    第一次进入时,pred的节点中的waitStatus等于0,所以会进入else代码块,然后通过 compareAndSetWaitStatus(pred, ws, Node.SIGNAL) 方法,将p节点的waitStatus值设置成SIGNAL, 返回false,退出shouldParkAfterFailedAcquire方法.

     

    acquireQueued()内部的for(;;)循环继续执行, p还是head节点,然后调用tryAcquire()方法又去抢一次锁,如果此时10号线程释放了锁资源, tryAcquire抢占资源有可能会成功,不过,此时咱们10号线程还持有锁资源,所以20线程本次抢占锁资源肯定会失败. 

     

    20号线程第二次抢占锁资源失败.

    然后又会进入shouldParkAfterFailedAcquire(p,node)方法, 不过这次p节点的waitStatus值为SIGNAL, 所以该方法返回true.

    因为shouldParkAfterFailedAcquire(p,node)方法返回true,所以程序就可以进入到parkAndCheckInterrupt()方法了.

     

     

     

    请记住,20号线程是卡LockSupport.park方法上的.

    接着我们让30号线程来抢占资源

     

    (3) 30号线程抢占锁资源 

    因为大体逻辑与前面20线程抢占锁资源一样,所以略过一些渣渣逻辑, 看一些关键地方.

     

    1. addWaiter方法

    与20号线程不同是,20号线程执行addWaiter方法时,CLH中无没有节点,而此时CLH中的tail节点就是20号线程的node节点.请看载图

     

    逻辑进入到if代码块中, 这儿主要就是干了一件事,将新创建的30号线程node挂到原先20号线程node节点后面,即CLH中,30号线程node成功尾节点

     

     

    addWaiter方法执行完毕.

     

    2. acquireQueued方法


    此时,acquireQueued方法入参节点是30线程节点,由上面链表图或debug截图可知,它的前一个节点node是20号线程节点, 并非head节点. 所以,程序会进入到shouldParkAfterFailedAcquire(p,node)方法.  这次p是20号线程节点, node是30号线程节点. 在该方法内部会将20号线程node的waitStattus修改成SIGNAL,然后通过acquireQueued的for循环进入到parkAndCheckInterrupt()方法. 在该方法中,会将当前线程30号线程park挂起.

     

    到此为止,50个线程的情况是: 10号线程持有锁 , 20号线程和30线程在CLH链表中, 处理WAIT状态.

     

    (4)40号线程抢占锁资源 

    与前30号线程抢占锁资源真的是完全一样了,只是此时40号线程的node会是CLH链表的tail节点.

     

    到此为此,挂起的线程有20号线程,30号线程,40号线程

    下面记录释放锁的流程

     

    (5)10号线程释放锁资源

    释放锁资源总的来说,逻辑比较简单

    protected final boolean tryRelease(int releases) {
        // AQS的state属性值减1
        int c = getState() - releases;
        // 如果当前线程不等于AQS中的exclusiveOwnerThread线程抛个异常
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) { 
            free = true;
            // 将AQS的exclusiveOwnerThread属性值设置为null
            setExclusiveOwnerThread(null);
        }
        // 修改AQS的state属性值
        setState(c);
        return free;
    }

    如果tryRelease方法返回true, 说明10线程释放锁资源成功.如果这时50号线程去抢占锁资源,完成有可能抢锁成功. 对于20,30,40这个三个线程而言,它们仍然处于park状态,并没有被唤醒. 为了验证上面所说的,我现在让50号线程去抢占锁资源.

    所以, 上面的结论完全正确. 那么release()方法后面的代码(标红部分)是干嘛的呢?

        public final boolean release(int arg) {
            if (tryRelease(arg)) {
                Node h = head;
                if (h != null && h.waitStatus != 0)
                    unparkSuccessor(h);
                return true;
            }
            return false;
        }

    咱们接着分析unparkSuccessor方法

    private void unparkSuccessor(Node node) {
        // 头节点的waitStatus值
        int ws = node.waitStatus;
        if (ws < 0)
            // 如果头节点waitStatus值小于0,通过cas设置成0
            compareAndSetWaitStatus(node, ws, 0);
    
        // 获取头节点的下一个节点s
        Node s = node.next;
        // 如果下一个节s是null 或者是取消状态,那么就需要找出真正可以唤醒unpark的线程节点
        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)
            // unpark唤醒s节点的thread
            LockSupport.unpark(s.thread);
    }

    F8执行之后,咱们debug切换到20号线程.

    然后这个20号线程仍然会在acquireQueued方法中运行,尝试着去抢占锁资源.  大家别忘acquireQueued方法中的for死循环哟~~~~~~~~~~就是靠着这个死循环去抢锁资源的.......

    当然AQS不会让它一直去抢锁资源,它会通过waitStatus这个属性值去控制, 如果当前线程的waitStatus值是SIGNAL,那么对不起,就不能让你再去抢资源了,

    把你park吧. 等下次有线程将你unpark之后, 你再来抢一次锁资源吧,如果还是抢不到,继续park吧!

    注意: 不是所有被unpark的线程都有资格去tryAcquire锁资源,有仅且有是位head节点的下一个节点才有资格.

    言归正传,20号线程抢了一次锁资源,但是没抢到,所以继续park吧

    (6)让50号线程释放锁资源

     

    下面我让50号线程释放锁资源,让park着的线程去抢占锁资源,看看是啥情况?

    按照前面的套路, 这时debug应该切换到20号线程

    由于锁资源处于空置状态 , 所以20号线程肯定可以抢占成功.

    20号线程抢锁成功, 从而跳出acquireQueued()方法中的死循环, 然后执行自定义的业务逻辑,本例就是20号线程也可以sleep(1000).

    还有一点值得注意的是, 20号线程抢到锁了,从会从CLH链表移除到,所以此时CLH链表中是head, 30号线程, 40号线程.

    30号线程同理可知,  最后我们来看看AQS是如何来处理CLH链表中的最后一个节点,40号线程的

    (7) 40号线程抢占锁资源 

    本质上与前面没啥不同, 唯一的区别就是,因为40号线程是CLH中最后一个node,它抢占到锁之后,也会从CLH链表中移除.

    好, 到这里,AQS的独占锁原理应该是分析明白了, 下面总结一下关键信息点

    (8) 总结

    1. AQS使用 exclusiveOwnerThread 来表示占有锁资源的线程
    2. AQS使用state表示锁资源是否被占用, 重入锁就是使用state与exclusiveOwnerThread 配合来实现 
    3. AQS会引用两个Node节点,分别是head与tail, head与tail都是懒加载实现的, setHead方法是唯一能修改CLH链表头节点的方法, addWaiter(enq)就是唯一能修改尾节点的方法
    4. AQS通过head节点去抢占锁资源(其实质是通过head节点去操作head节点的next节点)
    5. AQS通过head节点去唤醒park线程(因为存在着边际条件, 所以用到Node的waitState属性值作判断)
    6. 抢到锁资源的node线程节点,会从CLH链表中移除.
    7. 线程在释放掉锁资源的同时,也会unpark挂起的线程, 而且这个被unpark的线程一定是head节点next指向的线程节点
    8. 被unpark的线程,拥有一次抢占锁资源的资格,如果抢占失败,又会被park,直到被释放锁资源的线程再次unpark
    9. CLH链表中,越是靠近head节点的线程,就越早有可能被unpark,然后尝试抢占锁资源
    10. 每个新加入的线程,都有一次抢占锁资源的机会,如果抢占失败,就会通过addWaiter方法,挂到CLH链表的尾部,这时,它只有被unpark了,才有机会去抢占锁资源 
    11. Node节点的waitStatus有4个属性值, 比较重要就是0和-1, 当waitStatus等于-1时,表示这个线程可以安全的park挂起,  而在unpark时,waitStatus值又会由-1 就成0.

        在shouldParkAfterFailedAcquire方法中.将head节点的waitStatus值设置成SIGNAL, 以便于后面park线程
        在unparkSuccessor方法中,会将head节点的值由SIGNAL修改成0,然后去unpark挂起的线程

     暂时就到这儿吧, 以后有补充了....

  • 相关阅读:
    Java WebService 简单实例
    WCF、WebAPI、WCFREST、WebService之间的区别
    SpringMVC下的Shiro权限框架的使用
    SpringMVC详细示例实战教程
    JPA入门例子(采用JPA的hibernate实现版本)
    如果redis没有设置expire,他是否默认永不过期?
    jvm 年轻代、年老代、永久代
    Consumer group理解深入
    Java中GC的工作原理
    Flink 集群运行原理兼部署及Yarn运行模式深入剖析
  • 原文地址:https://www.cnblogs.com/z-qinfeng/p/12075493.html
Copyright © 2020-2023  润新知