• 简单纪要:浅谈ReentrantLock的父亲AbstractQueuedSynchronizer(AQS)的底层实现


    在日常开发中,我们都直接或间接的使用过ReentrantLock,以及他的兄弟们Semaphore,CountdownLatch,ReadwriteLock,但是对于其底层是如果实现锁机制的呢?

    下面我们先来看一张图:

       

    大家可能在想,这张图里面并没有看到我们知道ReentrantLock,甚至连Lock我们也没有看到,不急,我可以先new一个Lock来看看:

     Lock lock = new ReentrantLock();  
    /**
         * Creates an instance of {@code ReentrantLock}.
         * This is equivalent to using {@code ReentrantLock(false)}.
         */
        public ReentrantLock() {
            sync = new NonfairSync();
        }

    现在想必大家都明白了,我们在创建ReentrantLock的对象的时候 ,其实是创建了一个NonfairSync()对象,NonfairSync类是静态内部类,他继承了Sync,Sync继承了我们今天的主角AbstractQueuedSynchronizer;

    现在我们先不急于看源码,我可以先看一张图,毕竟图是最直观的,走起,看图~

    AQS是将每一条请求共享资源的线程封装为一个CLH锁队列的节点,AQS就是基于CLH实现的,内部维护了一个volatile int state 字段,线程通过CAS修改同步资源状态,成功则获取锁成功,失败则进入等待队列,等待唤醒;

    volatile int state 字段可以通过三种方式操作:

    • getState();
    • setState();
    • compareAndSetState();

    AQS实现了两种资源共享的方式:独占(Exclusive)和共享(Share);

     1. 从acquire(int arg)开始调取AQS中的程序,调取tryAcquire()方法获取同步资源(state)的状态,判断当前锁state>=0才可以获取到锁,获取一次,计数器进行 + 1 ,释放一次 -1 ;

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

     2. 如果tryAcquire()获取同步资源state 小于0 ,返回失败;调取addWaiter(Node)方法,将当前线程放入等待队列的队尾,并返回当前队列;

    将当前线程包装为Node,执行入队操作,将当前节点插入到等待队列的队尾;成功,返回当前线程包装为的node;

    失败,调取enq(Node)方法;

    private Node addWaiter(Node mode) {
            Node node = new Node(Thread.currentThread(), mode);
            // Try the fast path of enq; backup to full enq on failure
            Node pred = tail;
            if (pred != null) {
                node.prev = pred;
                if (compareAndSetTail(pred, node)) {
                    pred.next = node;
                    return node;
                }
            }
            enq(node);
            return node;
        }

    3. 失败,调取enq(Node)方法,此方法  自旋 直到成功直接使用CAS操作 ,只能返回一个节点;

    首先判断尾节点是否为空,就是判断队列是否初始化,因为 队列必须初始化, 当队列未初始化,创建一个空的标志结点作为head结点,并将tail也指向head;

    如果队列初始化成功,使用CAS保证只有一个头节点初始化成功;

    private Node enq(final Node node) {
            for (;;) {
                Node t = tail;
                if (t == null) { // Must initialize
                    if (compareAndSetHead(new Node()))
                        tail = head;
                } else {
                    node.prev = t;
                    if (compareAndSetTail(t, node)) {
                        t.next = node;
                        return t;
                    }
                }
            }
        }

    4. addWaiter()方法执行完毕后,节点已经被加入到等待队列当中,当前线程处于挂起状态,等待被唤醒;调用acquireQueued()方法,返回获取资源的状态;

    获取当前节点的前驱节点是  头节点,并尝试获取同步资源的状态成功,调用setHead()将当前节点设为头节点,setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!

    如果当前节点不是头节点,执行 shouldParkAfterFailedAcquire(p, node)方法,

    final boolean acquireQueued(final Node node, int arg) {
          //是否获取到资源
    boolean failed = true; try {
            //是否等待过程被中断过
    boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }

    如果当前节点不是头节点,执行 shouldParkAfterFailedAcquire(p, node)方法处理获取锁失败的挂起逻辑,获取前驱的状态pred.waitStatus,如果已经设置如果前驱节点拿到资源后告诉的自己,即已经设置Node.SIGNAL,则可以进入等待状态,如果前驱节点已经取消,就跳过当前的前驱节点,到找到最近一个正常等待的状态,并排在它的后边,则之前的前驱节点无引用,则会被GC回收。

    如果前驱节点状态正常,就设置前驱节点的状态为SINGAL,获取到资源后通知;

     private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
            int ws = pred.waitStatus;
            if (ws == Node.SIGNAL)
                /*
                 * This node has already set status asking a release
                 * to signal it, so it can safely park.
                 */
                return true;
            if (ws > 0) {
                /*
                 * Predecessor was cancelled. Skip over predecessors and
                 * indicate retry.
                 */
                do {
                    node.prev = pred = pred.prev;
                } while (pred.waitStatus > 0);
                pred.next = node;
            } else {
                /*
                 * waitStatus must be 0 or PROPAGATE.  Indicate that we
                 * need a signal, but don't park yet.  Caller will need to
                 * retry to make sure it cannot acquire before parking.
                 */
                compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
            }
            return false;
        }

     如果shouldParkAfterFailedAcquire()方法返回ture,证明前驱节点的状态已经设置好,调用parkAndCheckInterrupt()方法; 

    private final boolean parkAndCheckInterrupt() {
            LockSupport.park(this);// 调用park(),线程进入waiting状态
            return Thread.interrupted();//返回线程中断状态
        }

    此时,AQS已经大体看完了,此时再看看我们开篇提供的流程图,想必大家理解的会更加深刻,希望能对大家有所帮助,有问题请指出哦^_^~

     

  • 相关阅读:
    STM32学习(I2C SPI)
    STM32学习2(GPIO EXTI SYSTICK 通信基本概念)
    STM32学习1(RCC时钟树)
    C语言学习日记8
    C语言学习日记7
    C语言学习日记6
    C语言学习日记5
    C语言学习日记4
    java-课程设计-彩票购买抽奖程序
    java-课程设计-彩票购买抽奖程序(个人部分)
  • 原文地址:https://www.cnblogs.com/Rnan/p/11962259.html
Copyright © 2020-2023  润新知