美文网首页
ReentrantReadWriteLock是公平锁?

ReentrantReadWriteLock是公平锁?

作者: ssochi | 来源:发表于2020-01-14 21:17 被阅读0次

    证明

    new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("acquire read lock 1");
                    lock.readLock().lock();
                    System.out.println("get read lock 1");
    
                    sleep(200);
    
                    lock.readLock().unlock();
                    System.out.println("release read lock 1");
                }
            }).start();
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    sleep(100);
    
                    System.out.println("acquire write lock 1");
                    lock.writeLock().lock();
                    System.out.println("get write lock 1");
    
                    sleep(400);
    
                    lock.writeLock().unlock();
                    System.out.println("release write lock 1");
                }
            }).start();
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    sleep(300);
                    System.out.println("acquire read lock 2");
                    lock.readLock().lock();
                    System.out.println("get read lock 2");
                }
            }).start();
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    sleep(400);
                    System.out.println("acquire write lock 2");
                    lock.writeLock().lock();
                    System.out.println("get write lock 2");
                }
            }).start();
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    sleep(500);
                    System.out.println("acquire read lock 3");
                    lock.readLock().lock();
                    System.out.println("get read lock 3");
                }
            }).start();
    

    这段代码陆续加了5把锁,加减锁的顺序分别是:

    1. 加1号写锁
    2. 加1号读锁
    3. 加2号写锁
    4. 加2号读锁
    5. 释放一号写锁
      我们来预测一下这五把锁的获取顺序是怎样的
      首先读写锁并没持有任何锁,因此1号写锁成功获取锁,
      接下来2,3,4步由于读写锁已经持有写锁,因此后三把锁只能等待
      接下来释放1号写锁,基于公平锁的原则,排在最前面的1号读锁会马上获取锁
      而根据只要有一把写锁,别的写锁也能成功加锁的原则,
      2号写锁也会成功加锁,
      这时只有2号写锁还在等待状态。
      然而事实是这样的吗?
    acquire write lock 1
    get write lock 1
    acquire read lock 1
    acquire write lock 2
    acquire read lock 2
    release write lock 1
    get read lock 1
    

    以上为控制台的输出结果,可见在1号写锁释放锁之后,只有1号读锁成功获取了锁,而2号读锁只有等着。
    这显然是违反公平锁的原则,若干在1号锁获取锁成功后,别的线程来加读锁,无疑是会成功的,而2号读锁只能傻傻的等着,这不是欺负老实人吗。
    所有这是JDK的逻辑bug!

    原因

    让我们读读源码看看为啥会这样

    /**
             * Acquires the read lock.
             *
             * <p>Acquires the read lock if the write lock is not held by
             * another thread and returns immediately.
             *
             * <p>If the write lock is held by another thread then
             * the current thread becomes disabled for thread scheduling
             * purposes and lies dormant until the read lock has been acquired.
             */
            public void lock() {
                sync.acquireShared(1);
            }
    

    首先获取写锁时,会调用AQS的acquireShared方法

     /**
         * Acquires in shared uninterruptible mode.
         * @param arg the acquire argument
         */
        private void doAcquireShared(int arg) {
            final Node node = addWaiter(Node.SHARED);
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (;;) {
                    final Node p = node.predecessor();
                    if (p == head) {
                        int r = tryAcquireShared(arg);
                        if (r >= 0) {
                            setHeadAndPropagate(node, r);//(1)
                            p.next = null; // help GC
                            if (interrupted)
                                selfInterrupt();
                            failed = false;
                            return;
                        }
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt()//(2))
                        interrupted = true;
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    

    而acquireShared方法会调用doAcquireShared,当一个线程获取到读锁的时候会调用setHeadAndPropagate方法

    /**
         * Sets head of queue, and checks if successor may be waiting
         * in shared mode, if so propagating if either propagate > 0 or
         * PROPAGATE status was set.
         *
         * @param node the node
         * @param propagate the return value from a tryAcquireShared
         */
        private void setHeadAndPropagate(Node node, int propagate) {
            Node h = head; // Record old head for check below
            setHead(node);
            /*
             * Try to signal next queued node if:
             *   Propagation was indicated by caller,
             *     or was recorded (as h.waitStatus) by a previous operation
             *     (note: this uses sign-check of waitStatus because
             *      PROPAGATE status may transition to SIGNAL.)
             * and
             *   The next node is waiting in shared mode,
             *     or we don't know, because it appears null
             *
             * The conservatism in both of these checks may cause
             * unnecessary wake-ups, but only when there are multiple
             * racing acquires/releases, so most need signals now or soon
             * anyway.
             */
            if (propagate > 0 || h == null || h.waitStatus < 0) {
                Node s = node.next;
                if (s == null || s.isShared())
                    doReleaseShared();
            }
        }
    

    setHeadAndPropagate方法很简单,它会尝试去唤醒队列中下一个的处于Shared状态的节点。而下一个节点被唤醒时,它应该处于doAcquireShared的parkAndCheckInterrupt这个位置。它进而也会调用setHeadAndPropagate,来唤醒下一个Shared状态的节点。这样一直唤醒,直到遇见了一个Excluse状态的节点,这种连锁唤醒行动就停止了。

    Shared1 ->Shared2 ->Shared3 ->Exclusive1->Shared4

    如上队列Shared1引发的连锁唤醒便不能唤醒Shared4,因此导致了我们观察到的非公平现象。

    为什么会这样

    这并不会造成严重的后果,之所以JDK会采用如下的实现方式,主要是由于AQS的局限性。因为AQS并不能提供后排插到前排的机制。AQS的队列调度使用了CAS,因此仅仅修改队首和队尾时只用考虑一个变量,而从后排到前排则复杂的多,首先从后排删除同时要影响前后两个节点,而插入前排同样会影响两个节点。而如果想让exclusive后排的节点被唤醒,那么就需要便利整个队列,这是需要耗时的,CAS没法保证四个变量同时被修改,若在遍历途中读锁全释放了,那遍历如何取消?若遍历途中读锁被释放又加上,怎么保证同时只有一个遍历在进行?
    或许解决的办法只有加锁,AQS的state被用来存储同时被加的锁的数量,而当state=0时一般才能释放锁。那么只要在遍历开始时增加state,那么就能保证读锁在遍历结束前不会释放。
    但是这样做并不是解决之道,首先在AQS这种锁底层框架中用到锁本身就很影响性能,其次state是AQS面向它的继承者的一个方法,对AQS本身没有意义,如果将state和AQS依赖在一起,那么从逻辑上来说缺乏整体性,并且可能会影响别的基于AQS实现的同步工具,这是得不偿失的。

    结论

    ReentrantReadWriteLock不是公平锁,但是世界上哪有绝对的公平。社会总是在效率和公平中做出折中的选择,而JDK也是,这或许就是宇宙的哲学!

    相关文章

      网友评论

          本文标题:ReentrantReadWriteLock是公平锁?

          本文链接:https://www.haomeiwen.com/subject/plaaactx.html