zoukankan      html  css  js  c++  java
  • 可重入读写锁ReentrantReadWriteLock基本原理分析

    前言

    本篇适用于了解ReentrantLock或ReentrantReadWriteLock的使用,但想要进一步了解原理的读者。见于之前的分析都是借鉴大量的JDK源码,这次以流程图的形式代替源码,希望读者能有更好的阅读体验。有兴趣了解源码的读者也可以借鉴本篇的分析成果做源码分析。

    《从源码分析ReentrantLock原理》这一篇文章中分析了以非阻塞同步算法为基础实现的可重入独占锁ReentrantLock。所谓** “独占” 即同一时间只能有一个线程持有锁。而 “重入” **是指该线程如果持有锁,可以在同步代码块内再次请求占有锁而不被阻塞,线程重入后将AQS内部状态state同步加1继续同步区的操作。但是要注意该线程要想移交锁的控制权必须完全释放重入锁,即将AQS的state同步更新到0为止。

    ReentrantReadWriteLock出现的目的就是针对ReentrantLock独占带来的性能问题,使用ReentrantLock无论是“写/写”线程、“读/读”线程、“读/写”线程之间的工作都是互斥,同时只有一个线程能进入同步区域。然而大多实际场景是“读/读”线程间并不存在互斥关系,只有"读/写"线程或"写/写"线程间的操作需要互斥的。因此引入ReentrantReadWriteLock,它的特性是:** 一个资源可以被多个读操作访问,或者一个写操作访问,但两者不能同时进行。**从而提高读操作的吞吐量。

    初识ReentrantReadWriteLock

    ReentrantReadWriteLock并没有继承ReentrantLock,也并没有实现Lock接口,而是实现了ReadWriteLock接口,该接口提供readLock()方法获取读锁,writeLock()获取写锁。

    public class ReentrantReadWriteLock
            implements ReadWriteLock, java.io.Serializable {
    
        private final ReentrantReadWriteLock.ReadLock readerLock;
        private final ReentrantReadWriteLock.WriteLock writerLock;
    
        public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
        public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }
    }
    
    public interface ReadWriteLock {
        
        Lock readLock();
    
        Lock writeLock();
    }
    

    默认构造方法为** 非公平模式 ,开发者也可以通过指定fair为true设置为 公平模式 **。

        public ReentrantReadWriteLock() {
            this(false);
        }
    
        public ReentrantReadWriteLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
            readerLock = new ReadLock(this);
            writerLock = new WriteLock(this);
        }
    
        public static class ReadLock implements Lock, java.io.Serializable {}
        public static class WriteLock implements Lock, java.io.Serializable {}
    

    而公平模式和非公平模式分别由内部类FairSync和NonfairSync实现,这两个类继承自另一个内部类Sync,该Sync继承自AbstractQueuedSynchronizer(以后简称** AQS **),这里基本同ReentrantLock的内部实现一致。

    abstract static class Sync extends AbstractQueuedSynchronizer {
    }
    
    static final class FairSync extends Sync {
    }
    
    static final class NonfairSync extends Sync {
    }
    

    在ReentrantLock的分析中得知,其独占性和重入性都是通过CAS操作维护AQS内部的state变量实现的。ReentrantReadWriteLock将这个int型state变量分为高16位和低16位,高16位表示当前读锁的占有量低16位表示写锁的占有量,详见ReentrantReadWriteLock的内部类Sync :

    abstract static class Sync extends AbstractQueuedSynchronizer {
            static final int SHARED_SHIFT   = 16;
            static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
            static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
            static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
    
            /** Returns the number of shared holds represented in count  */
            static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
            /** Returns the number of exclusive holds represented in count  */
            static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
            ...
    }
    

    读锁分析

    读锁,锁定的是AQS的state变量的高16位,当state的高16位等于0,表示当前读锁未被占有;当state的高16位大于0,表示当前读锁可能被一个或多个线程占有,多于一个占有读锁的线程,允许重入。

    读锁竞争

     
    读锁竞争过程.png

    读锁的获取条件要满足:

    1. ** 当前的写锁未被占有(AQS state变量低16位为0) 或者当前线程是写锁占有的线程**
    2. ** readerShouldBlock()方法返回false **
    3. ** 当前读锁占有量小于最大值(2^16 -1) **
    4. ** 成功通过CAS操作将读锁占有量+1(AQS的state高16位同步加1) **

    条件1使得读锁与写锁互斥,除非当前申请读操作的线程是占有写锁的线程,即实现了写锁降级为读锁。

    条件2在非公平模式下执行的是NonfairSync类的readerShouldBlock()方法:

        final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();
        }
    
        final boolean apparentlyFirstQueuedIsExclusive() {
            Node h, s;
            return (h = head) != null &&
                (s = h.next)  != null &&
                !s.isShared()         &&
                s.thread != null;
        }
    

    如果AQS的锁等待队列head节点后的节点非共享节点(等待读锁的节点),将返回true。

    条件2在公平模式下执行的是FairSync类的readerShouldBlock方法:

        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    
        public final boolean hasQueuedPredecessors() {
            Node t = tail; // Read fields in reverse initialization order
            Node h = head;
            Node s;
            return h != t &&
                ((s = h.next) == null || s.thread != Thread.currentThread());
        }
    

    只要AQS锁等待队列的头尾不为空,并且存在head后的节点并且节点的线程非当前线程,返回true。

    条件3保证读锁的占有数不超过最大上限,条件4保证多线程竞争读锁时的安全性。

    不满足条件申请读锁的线程会被封装为SHARED类型的线程节点插入到AQS锁等待队列的末尾,在插入队列尾后还有一次机会尝试获取读锁。如果还是失败的,下面判断如果队列前一节点是SIGNAL状态就将线程挂起。当线程唤醒后会再次尝试获取读锁,不满足条件会再次挂起,以此循环。

    ** 如果在线程挂起前获取读锁,下面会将当前节点设置为head节点,并将head后的SHARED类型的节点的唤醒。然后进入读锁同步区域。被唤醒的线程会继续尝试获取读锁,获取读锁成功后就继续上述步骤,这样就保证了队列中几个连续的等待读锁的线程被依次唤醒进入读锁同步区。**

    读锁释放

     
    读锁的释放.png

    读锁的释放过程即AQS的state高16位同步递减为0的过程,当state的高16位都为0表示读锁释放完毕,如果此时写锁状态为0(即该读锁不是写锁降级来的),唤醒head节点后下一个SIGNAL状态的节点的线程,一般为等待写锁的节点。如果读锁的占有数不为0,表示读锁未完全释放。或者写锁的占有数不为0,表示释放的读锁是写锁降级来的。

    写锁分析

    写锁的状态表示为AQS的state变量的低16位,当state低16位为0,表示当前写锁没有被占有,反之表示写锁被某个写线程占有(state = 1)或重入(state > 1)。

    写锁竞争

     
    写锁竞争过程.png

    写锁获取的条件需要满足:

    1. ** 读锁未被占用(AQS state高16位为0) ,写锁未被占用(state低16位为0)或者占用写锁的线程是当前线程**
    2. ** writerShouldBlock()方法返回false,即不阻塞写线程 **
    3. ** 当前写锁占有量小于最大值(2^16 -1),否则抛出Error("Maximum lock count exceeded") **
    4. ** 通过CAS竞争将写锁状态+1(将state低16位同步+1) **

    条件1使得写锁与读锁互斥,ReentrantReadWriteLock并没有读锁升级的功能。

    条件2的writerShouldBlock()方法在非公平模式下实现为:

            final boolean writerShouldBlock() {
                return false; // writers can always barge
            }
    

    即非公平模式下允许满足条件的写操作直接插队。

    条件2的writerShouldBlock()方法在公平模式下实现为:

            final boolean writerShouldBlock() {
                return hasQueuedPredecessors();
            }
    

    公平模式下同读锁一样,如果AQS的锁等待队列不为空,写操作无法插队。

    条件3保证写锁占有线程的重入次数不会溢出上限,条件4保证多个写操作的线程竞争写锁的安全性。

    不满足获取写锁条件的线程会封装为EXECLUSIVE型的NODE插入到AQS的锁等待队列尾部,通过acquireQueued方法进入循环,该循环内再次尝试获取写锁(因为经过上述操作,另一个锁占有线程可能释放了锁),否则通过shouldParkAfterFailedAcquire方法将前一节点设置为SIGNAL状态后将自身线程挂起。当线程被唤醒后会再次尝试获取写锁,失败则继续挂起,以此循环。或成功占有写锁则将当前Node设置为head节点,返回中断标记并进入同步代码区。** 与读操作不同的是写操作之间是互斥的,所以获取写锁后不会将下一个申请写操作的节点唤醒。**

    写锁释放

     
    写锁的释放.png

    写锁的释放过程即AQS的state低16位同步递减为0的过程,当state的高16位都为0表示写锁释放完毕,唤醒head节点后下一个SIGNAL状态的节点的线程。如果该写锁占有线程未释放写锁前还占用了读锁,那么写锁释放后该线程就完全转换成了读锁的持有线程。

    小结

    • 读锁的重入是允许多个申请读操作的线程的,而写锁同时只允许单个线程占有,该线程的写操作可以重入。
    • 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
    • 对于同时占有读锁和写锁的线程,如果完全释放了写锁,那么它就完全转换成了读锁,以后的写操作无法重入,在写锁未完全释放时写操作是可以重入的。
    • 公平模式下无论读锁还是写锁的申请都必须按照AQS锁等待队列先进先出的顺序。非公平模式下读操作插队的条件是锁等待队列head节点后的下一个节点是SHARED型节点,写锁则无条件插队。
    • 读锁不允许newConditon获取Condition接口,而写锁的newCondition接口实现方法同ReentrantLock。


    作者:Mars_M
    链接:https://www.jianshu.com/p/9f98299a17a5
    來源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
  • 相关阅读:
    NET5 ORM 六大新功能
    牛逼程序员必须要掌握金字塔思维
    实体类转Json的2种方法
    怎么使用jquery阻止页面的离开或卸载
    GitHub的用法:到GitHub上部署项目
    搭建个人服务器
    远程服务器上部署本地项目
    java.nio.ByteBuffer中flip,rewind,clear方法的区别
    eclipse Run On Server 异常:could not load the Tomcat Server configuration at Servers omcat V5.0 Sertomcat
    throw与throws的区别
  • 原文地址:https://www.cnblogs.com/GarfieldEr007/p/10224170.html
Copyright © 2011-2022 走看看