ReentrantLock源码解析1--获得非公平锁与公平锁lock()下篇

勿忘初心2018-12-19 10:06

此文已由作者赵计刚授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。


3.3.3、AbstractQueuedSynchronizer:acquireQueued(final Node node, int arg)

    final boolean acquireQueued(final Node node, int arg) {
        try {
            boolean interrupted = false;
            /*
             * 无限循环(一直阻塞),直到node的前驱节点p之前的所有节点都执行完毕,p成为了head且node请求成功了
             */
            for (;;) {
                final Node p = node.predecessor();//获取插入节点的前一个节点p
                /*
                 * 注意:
                 * 1、这个是跳出循环的唯一条件,除非抛异常
                 * 2、如果p == head && tryAcquire(arg)第一次循环就成功了,interrupted为false,不需要中断自己
                 *         如果p == head && tryAcquire(arg)第一次以后的循环中如果执行了挂起操作后才成功了,interrupted为true,就要中断自己了
                 */
                if (p == head && tryAcquire(arg)) {
                    setHead(node);//当前节点设置为头节点
                    p.next = null; 
                    return interrupted;//跳出循环
                }
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    interrupted = true;//被中断了
            }
        } catch (RuntimeException ex) {
            cancelAcquire(node);
            throw ex;
        }
    }

AbstractQueuedSynchronizer:shouldParkAfterFailedAcquire(Node pred, Node node)

    /**
     * 检测当前节点是否可以被安全的挂起(阻塞)
     * @param pred    当前节点的前驱节点
     * @param node    当前节点
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;//获取前驱节点(即当前线程的前一个节点)的等待状态
        if (ws == Node.SIGNAL)//如果前驱节点的等待状态是SIGNAL,表示当前节点将来可以被唤醒,那么当前节点就可以安全的挂起了
            return true;
        /*
         * 1)当ws>0(即CANCELLED==1),前驱节点的线程被取消了,我们会将该节点之前的连续几个被取消的前驱节点从队列中剔除,返回false(即不能挂起)
         * 2)如果ws<=0&&!=SIGNAL,将当前节点的前驱节点的等待状态设为SIGNAL
         */
        if (ws > 0) {
            do {
                /*
                 * node.prev = pred = pred.prev;
                 * 上边这句代码相当于下边这两句
                 * pred = pred.prev;
                 * node.prev = pred;
                 */
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * 尝试将当前节点的前驱节点的等待状态设为SIGNAL
             * 1/这为什么用CAS,现在已经入队成功了,前驱节点就是pred,除了node外应该没有别的线程在操作这个节点了,那为什么还要用CAS?而不直接赋值呢?
             * (解释:因为pred可以自己将自己的状态改为cancel,也就是pred的状态可能同时会有两条线程(pred和node)去操作)
             * 2/既然前驱节点已经设为SIGNAL了,为什么最后还要返回false
             * (因为CAS可能会失败,这里不管失败与否,都返回false,下一次执行该方法的之后,pred的等待状态就是SIGNAL了)
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

AbstractQueuedSynchronizer:

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//挂起当前的线程
        return Thread.interrupted();//如果当前线程已经被中断了,返回true
    }

 

以上就是一个线程获取非公平锁的整个过程(lock())。

 

4、公平锁的lock()

具体用法与非公平锁一样

如果掌握了非公平锁的流程,那么掌握公平锁的流程会非常简单,只有两点不同(最后会讲)。

 

简化版的步骤:(公平锁的核心)

获取一次锁数量,

B1、如果锁数量为0,如果当前线程是等待队列中的头节点,基于CAS尝试将state(锁数量)从0设置为1一次,如果设置成功,设置当前线程为独占锁的线程;

B2、如果锁数量不为0或者当前线程不是等待队列中的头节点或者上边的尝试又失败了,查看当前线程是不是已经是独占锁的线程了,如果是,则将当前的锁数量+1;如果不是,则将该线程封装在一个Node内,并加入到等待队列中去。等待被其前一个线程节点唤醒。

 

源代码:

4.1、ReentrantLock:lock()

    /**
     *获取一个锁
     *三种情况:
     *1、如果当下这个锁没有被任何线程(包括当前线程)持有,则立即获取锁,锁数量==1,之后被唤醒再执行相应的业务逻辑
     *2、如果当前线程正在持有这个锁,那么锁数量+1,之后被唤醒再执行相应的业务逻辑
     *3、如果当下锁被另一个线程所持有,则当前线程处于休眠状态,直到获得锁之后,当前线程被唤醒,锁数量==1,再执行相应的业务逻辑
     */
    public void lock() {
        sync.lock();//调用FairSync的lock()方法
    }

4.2、FairSync:lock()

        final void lock() {
            acquire(1);
        }

4.3、AbstractQueuedSynchronizer:acquire(int arg)就是非公平锁使用的那个方法

4.3.1、FairSync:tryAcquire(int acquires)

        /**
         * 获取公平锁的方法
         * 1)获取锁数量c
         * 1.1)如果c==0,如果当前线程是等待队列中的头节点,使用CAS将state(锁数量)从0设置为1,如果设置成功,当前线程独占锁-->请求成功
         * 1.2)如果c!=0,判断当前的线程是不是就是当下独占锁的线程,如果是,就将当前的锁数量状态值+1(这也就是可重入锁的名称的来源)-->请求成功
         * 最后,请求失败后,将当前线程链入队尾并挂起,之后等待被唤醒。
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (isFirst(current) && compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

最后,如果请求失败后,将当前线程链入队尾并挂起,之后等待被唤醒,下边的代码与非公平锁一样。

 

总结:公平锁与非公平锁对比

  • FairSync:lock()少了插队部分(即少了CAS尝试将state从0设为1,进而获得锁的过程)
  • FairSync:tryAcquire(int acquires)多了需要判断当前线程是否在等待队列首部的逻辑(实际上就是少了再次插队的过程,但是CAS获取还是有的)。

最后说一句,

  • ReentrantLock是基于AbstractQueuedSynchronizer实现的,AbstractQueuedSynchronizer可以实现独占锁也可以实现共享锁,ReentrantLock只是使用了其中的独占锁模式
  • 这一块儿代码比较多,逻辑比较复杂,最好在阅读的过程中,可以拿一根笔画画入队等与数据结构相关的图
  • 一定要记住"简化版的步骤",这是整个非公平锁与公平锁的核心


免费领取验证码、内容安全、短信发送、直播点播体验包及云服务器等套餐

更多网易技术、产品、运营经验分享请点击