美文网首页
多线程基础(十八):ReentrantReadWriteLock

多线程基础(十八):ReentrantReadWriteLock

作者: 冬天里的懒喵 | 来源:发表于2020-11-09 18:39 被阅读0次

    [toc]

    1.类结构及注释

    1.1 类结构

    ReentrantReadWriteLock是基于AQS实现的可重入的读写锁。这个锁在使用的时候将锁分为了两个部分,ReadLock和WriteLock。实际上这两个锁都是共同引用的一个AQS对象,共用了一个AQS队列。其与ReentrantLock一样,具有公平/非公平的特性,以及可重入等功能。其类结构如下图所示:


    image.png

    可以看到ReentrantReadWriteLock实现了ReadWriteLock接口,其内类Sync是实现读写锁的关键,Sync继承了AQS类,然后再由子类NonfairSync和FairSync两个实现,用以做公平锁和非公平锁的实现,再使用的时候根据构造函数来决定sync具体采用哪个实现类。而ReadLock和WriteLock则都持有sync的实现,读锁和写锁将共用AQS队列。

    1.2 注释说明

    ReentrantReadWriteLock是ReadWriteLock的一个实现,其具有ReentrantLock类似的语义,此类有如下属性:

    • 获得有序性
      这个类没有强制读锁或者写锁的顺序,但是,它确实支持一个可选的公平策略。
      非公平模式(默认的情况下):
      当构造为非公平锁的时候,不受重入限制,未指定读写锁的进入顺序。不断竞争的非公平锁可能会无期限的延迟一个或者多个读或者写的线程,但通常会比公平锁具备更高的吞吐量。
      如果采用公平的模式构造,线程采用近似到达策略竞争。当持有的锁被释放的时候,等待时间最长的一个写线程将被分配到锁。或者,如果有一组读线程等待的时间长于所有正在等待的写线程,这种情况下将被分配读锁。
      如果一个持有写锁或者有一个正在等待写操作的线程,试图(采用非重入)获取公平读锁的线程将阻塞。直到当前等待时间最久的写线程获得并释放写锁之后,该线程才会获得读锁。当然,如果等待中的写锁放弃了等待,而将一个或多个读线程做为队列中最长的waiter而没有写锁定,则将为这些读分配读锁定。
      试图获取公平的写锁(非可重入)的线程将阻塞,除非读锁或写锁均是空闲的(这意味着没有等待的线程)。请注意,非阻塞的ReadLock#tryLock()和 WriteLock#tryLock()不遵循此公平设置,并且如果可能的话,将立即获取锁定,而与等待线程无关。

    • 重入性
      此锁允许读和写以ReentrantLock的方式重新获取读或写的锁。在释放写与持有的所有写锁之前,不允许对读操作重入。
      此外,写可以获得读锁,反之则不能,在其他引用程序中,当在调用或者对在读锁下执行读的方法的过程中保持写锁时,重入操作会很有用。如果读试图获取写锁,纳秒它将永远不成功。

    • 锁降级
      重入锁允许将写锁降级到读锁,通过获得写锁,当需要读锁的时候,释放写锁,然而,通过读锁升级到写锁是不支持的。

    • 条件变量支持:
      写锁提供了一种Condition的实现,该实现的行为与写锁相同,与ReentrantLock#newCondition为ReentrantLock提供的Condition实现一样。当然,此Condition只能与写锁一起使用。
      读锁不支持Condition,readLock().newCondition()将抛出异常UnsupportedOperationException。

    • 监控:
      此类支持确定是持有锁还是争用锁的方法,这些方法的设计用于监视锁的状态,而不用于锁的控制。

    此类的序列化与内置锁的行为相同:反序列化的锁处于解锁状态,而不管序列化时的状态如何。

    示例用法:
    这是一个代码草图,显示了在更新缓存之后如何实现锁降级,(以嵌套方式处理多个锁的时候,异常处理非常棘手):

    class CachedData {
        Object data;
        volatile boolean cacheValid;
        final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
     
        void processCachedData() {
          rwl.readLock().lock();
          if (!cacheValid) {
            // Must release read lock before acquiring write lock
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
              // Recheck state because another thread might have
              // acquired write lock and changed state before we did.
              if (!cacheValid) {
                data = ...
                cacheValid = true;
              }
              // Downgrade by acquiring read lock before releasing write lock
              rwl.readLock().lock();
            } finally {
              rwl.writeLock().unlock(); // Unlock write, still hold read
            }
          }
     
          try {
            use(data);
          } finally {
            rwl.readLock().unlock();
          }
        }
      }}
    

    ReentrantReadWriteLocks 可以用于提高某些种类的Collection的并发性,仅当期望集合很大的时候,读线程大大超过写线程,并且需要的开销大于同步开销操作。如下是一个使用TreeMap的类,该类预计会很大并且可以并发访问。

    class RWDictionary {
        private final Map<String, Data> m = new TreeMap<String, Data>();
        private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        private final Lock r = rwl.readLock();
        private final Lock w = rwl.writeLock();
     
        public Data get(String key) {
          r.lock();
          try { return m.get(key); }
          finally { r.unlock(); }
        }
        public String[] allKeys() {
          r.lock();
          try { return m.keySet().toArray(); }
          finally { r.unlock(); }
        }
        public Data put(String key, Data value) {
          w.lock();
          try { return m.put(key, value); }
          finally { w.unlock(); }
        }
        public void clear() {
          w.lock();
          try { m.clear(); }
          finally { w.unlock(); }
        }
     }}
    

    实现注意事项:
    此锁最多支持65535个递归写锁和65535个读锁,尝试超过这些限制会抛出Error。

    2.成员变量及构造函数

    2.1 成员变量

    ReentrantReadWriteLock的成员变量如下表:

    变量名 类型 说明
    readerLock private final ReentrantReadWriteLock.ReadLock 用于读锁的实现类
    writerLock private final ReentrantReadWriteLock.WriteLock 用于写锁的实现类
    sync final Sync AQS的继承类,实现同步功能

    2.2 构造函数

    2.2.1 ReentrantReadWriteLock()

    默认的构造函数,实际上调用的是 ReentrantReadWriteLock(boolean fair)。在此可以看出实际上默认情况下是非公平锁。

    public ReentrantReadWriteLock() {
        this(false);
    }
    

    2.2.2 ReentrantReadWriteLock(boolean fair)

    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
    

    根据fair字段决定该锁的公平性,true将采用公平锁的实现,false则采用非公平锁的实现。

    3. 核心内部类Sync

    3.1 常量

    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;
    

    上述常量是用来实现读与写计数的常量,锁的状态在逻辑上将分为两个无符号的short类型存放,低位表示写锁(排他)的计数,高位表示读锁(共享)的计数。
    上面这个过程如下图所示:


    image.png

    结合count方法:

    /** 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; }
    

    可以发现,实际上就是将int字段c,分为高低位,高位存放读锁(共享)的计数,低位存放写锁(独占)的计数。


    image.png

    3.2 count计数器

    /**
     * 每个线程的读计数器,通过ThreadLocal,维护在cachedHoldCounter中。
     */
    static final class HoldCounter {
        int count = 0;
        // 使用final long 的id,用于避免垃圾保留,此处不是用的引用,而是常量,在GC的时候可以被回收。
        final long tid = getThreadId(Thread.currentThread());
    }
    //ThreadLocal的子类,重写了initialValue,避免当线程计数器不存在的时候,需要new之后再set。
    static final class ThreadLocalHoldCounter
        extends ThreadLocal<HoldCounter> {
        public HoldCounter initialValue() {
            return new HoldCounter();
        }
    }
    

    3.3 成员变量

     private transient ThreadLocalHoldCounter readHolds;
     private transient HoldCounter cachedHoldCounter;
     private transient Thread firstReader = null;
     private transient int firstReaderHoldCount;
    
    变量名 类型 说明
    readHolds private transient ThreadLocalHoldCounter 当前线程持有可重入读锁的数量,仅在构造函数和eadObejct中初始化,当线程的读取计数下降到0的时候将其删除。
    cachedHoldCounter private transient HoldCounter 成功获得读锁之后的最后一个线程的保留计数,在下一个要释放的线程是最后一个要获取线程的常见情况下,这可以节省去ThreadLocal查找。这不是volatile的,仅仅在缓存过程中使用。可以使正在为其缓存读取保留计数的线程超时,但是通过不保留对该线程的引用来避免垃圾保留。通过良性数据竞赛访问;依赖于内存模型中的final和 out-of-thin-air担保。
    firstReader private transient Thread firstReader是第一个获得读锁的线程,firstReaderHoldCount是firstReader的持有计数。更确切的说,firstReader是这样一个特殊的线程,它最后把共享计数器从0改为1(在锁空闲的时候)。并且此后没有释放读锁。如果没有这样的线程则返回null。除非线程在不放弃读锁的情况下终止,否则不会导致垃圾保留,因为tryReleaseShared将其设置为null。依赖于良性的数据竞争,依赖于内存模型的out-of-thin-air做为担保。这使得跟踪无竞争的读锁 的读操作非常经济。
    firstReaderHoldCount private transient int 见firstReader

    对于Counter,此处将使用ThreadLocal。需要注意的是sync此时采用了ThreadLocalHoldCounter继承ThreadLocal的方法,通过对initialValue重写,来new一个HoldCounter。
    构造函数如下:

    Sync() {
        readHolds = new ThreadLocalHoldCounter();
        setState(getState()); // ensures visibility of readHolds
    }
    

    3.4 抽象方法

    abstract boolean readerShouldBlock();
    
    abstract boolean writerShouldBlock();
    

    上述两个抽象方法,是公平锁和非公平锁实现类必须实现的方法,这是用于根据公平或者非公平锁的属性,来决定在读或者写过程中是否进行阻塞。

    3.5 抽象方法在NonfairSync与FairSync中的实现

    3.5.1 NonfairSync

    对于非公平锁的实现:

    final boolean writerShouldBlock() {
        //非公平模式下,写操作总是返回false,不会立即阻塞线程,尝试竞争锁一次锁。
        return false; // writers can always barge
    }
    final boolean readerShouldBlock() {
        //在非公平模式下的读操作,定义了一个启发式的方法,可以避免写入饥饿,如果队列的头线程存在,则阻塞。如果在其他启用的读取器后面还没有等待中的写入器还没有从队列中耗尽,那么新的读取器将不会阻塞。
        return apparentlyFirstQueuedIsExclusive();
    }
    

    3.5.2 FairSync

    对于公平锁,那么读或者是写操作,都需要根据队列中的数据来进行判断:

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

    在ReentrantReadWriteLock中,对于公平和非公平锁的区别即在于每次调用读或者写锁的操作的时候,判断对当前线程是否需要阻塞,在公平与非公平两种情况下的实现方式是有区别的。对于非公平的情况,对于写操作,直接返回false,意味着可以执行一次锁竞争操作。对于读操作则实现了一个启发式的方法。在公平模式下,读或者下的操作都需要判断在AQS队列中的前节点是否存在来进行。

    3.6 其他方法

    ReentrantReadWriteLock在此时继承AQS,同时实现了共享和独占两种模式。将共享模式应用于读锁,将独占模式应用于写锁。此时我没也可以理解为什么对于AQS没用将这些需要实现的方法定位为抽象方法的原因。
    主要的方法有:
    tryRelease
    tryAcquire
    tryReleaseShared
    tryAcquireShared
    fullTryAcquireShared
    tryWriteLock
    tryReadLock
    这些方法将在后续章节介绍读写锁的详细过程的时候来介绍其源码实现。

    4.实现类ReadLock与WriteLock

    ReentrantReadWriteLock封装了两个类,ReadLock和WriteLock来对应读和写的锁操作。

    4.1 ReadLock

    ReadLock类,内部引用了Sync,实际上就是将Sync的共享模式的相关方法包装为Lock。

    public static class ReadLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -5992448646407690164L;
        //引用 与writeLock共享 sync
        private final Sync sync;
    
         //构造函数,sync实际上就是lock的sync
        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
    }
    

    4.1.1 重要方法

    4.1.1.1 lock()

    获得读锁,如果读锁没用被任何其他线程持有,那么将立即返回。如果写锁由另外一个线程持有,则出于线程调度的目的,当前线程将被禁用,并出于休眠状态,知道获得读锁为止。

    public void lock() {
        sync.acquireShared(1);
    }
    
    4.1.1.2 lockInterruptibly()

    获得读锁,除非当前线程被interrupted。
    如果写锁未被其他的线程持有,则获取读锁,并立即返回。
    如果其他的线程已持有写锁,则出于线程调度的目的,当前线程将被禁用,并出于休眠状态,直到如下两种情况发生:

    • 当前线程获得读锁。
    • 其他的线程调用了当前线程的中断方法。

    如果当前线程在进入此方法之前已经被中断,则获得读锁的方法将被中断。然后抛出InterruptedException。并将中断状态清除。
    在此实现中,由于此方法是显式的中断点,因此优先于对中断的响应而不是正常或可重入的锁获取:

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
    
    4.1.1.3 tryLock()

    仅当调用另外一个线程未持有写锁的时候才获取读锁。
    如果写锁未被另外的线程持有,则获取读锁,并立即返回true。即便此锁设置未公平排序锁。无论当前是否正在等待其他线程,tryLock调用时都会立即获取读锁。用于读取锁定。这种“插入”是指即使在某些情况下,行为也会破坏公平性。如果要遵守此锁的公平性设置,请使用几乎等效的tryLock(long,TimeUnit)tryLock(0,TimeUnit.SECONDS),(它还会检测中断)。
    如果写锁由另一个线程持有,则此方法将立即返回true。

    public boolean tryLock() {
        return sync.tryReadLock();
    }
    
    4.1.1.4 tryLock(long timeout, TimeUnit unit)

    如果写锁未被其他的线程持有,且当前线程不处于中断状态,则获得读锁。

    如果写锁未被另一个线程持有,则获取读锁,并立即返回true。如果已将此锁设置为使用公平的排序策略,则如果有任何其他线程正在等待该锁,则将不会获取可用的锁。这与tryLock()方法相反。如果您想要一个定时的 tryLock确实允许在公平锁上插入,则将定时和非定时表单组合在一起:

    if (lock.tryLock() ||
         lock.tryLock(timeout, unit)) {
       ...
     }}
    

    如果写锁由另一个线程持有,则出于线程调度的目的,当前线程将被禁用,并处于休眠状态,直到发生以下三种情况之一:

    • 读锁由当前线程获取
    • 有其他的线程中断当前线程。
    • 指定的等待时间已超时。
    • 如果获取的读锁,则返回true.

    如果当前线程在进入此方法之前已设置了它的中断状态,将会抛出InterruptedException异常,并将中断状态清除。

    如果经过了指定的等待时间,则返回值 false。如果时间小于或等于零,则该方法将根本不等待。
    在此实现中,由于此方法是显式的中断点,因此优先于对中断的响应,而不是正常或可重入的锁定获取,而是优先报告等待时间的流逝。

    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }
    
    4.1.1.4 unlock()

    释放读锁。
    如果现在读取器的数量为零,则该锁定可用于写锁的tryLock。

    public void unlock() {
        sync.releaseShared(1);
    }
    
    4.1.1.5 newCondition()

    读锁不支持Condition,此处调用将抛出UnsupportedOperationException异常:

    public Condition newCondition() {
        throw new UnsupportedOperationException();
    }
    

    4.2 WriteLock

    WriteLock与ReadLock一样,也是在内部持有了Sync的引用。

    public static class WriteLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -4992448646407690164L;
        //与ReadLock共享都指向sync
        private final Sync sync;
        //构造函数
        protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
    }
    

    4.2.1 重要方法

    4.2.1.1 lock()

    获取写锁,如果此时读锁或写锁均未被使用,则此时获得写锁并立即返回,将写锁保持计数设置为1,如果当前线程已持有写锁,则计数加1,并立即返回。如果锁是由另外一个线程持有,则出于线程调度的目的,当前线程将被禁用,并出于WAIT状态,知道获得写锁为止,此时count被设置为1.

    public void lock() {
        sync.acquire(1);
    }
    
    4.2.1.2 lockInterruptibly()

    获得写锁,除非当前线程被中断。如果读锁和写锁此时均没用被其他线程使用,则此时将获得写锁,并立即返回,此时的计数状态设置为1.如果当前线程已经持有此锁,则持有计数将增加一,该方法将立即返回。如果锁是由另一个线程持有的,则出于线程调度的目的,当前线程将被禁用,并处于休眠状态,直到发生以下两种情况之一:当前线程获得写锁,其他的线程调用了此线程的中断方法。如果当前线程获取了写锁,则将锁保持计数设置为1。如果当前线程,在进入此方法时已设置其中断状态;获取写锁时被interrupted中断,将引起InterruptedException,并清除当前线程的中断状态。在此实现中,由于此方法是显式的中断点,因此优先于对中断的响应而不是正常或可重入的锁获取。

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    
    4.2.1.3 tryLock()

    在写锁没由被其他线程获取的时候调用,如果读锁或写锁均未被其他线程持有,则获得写锁,并立即返回。将count设置为1.即使此锁设置为公平锁,无论是否有其他线程正在等待,对于tryLock操作的调用都会立即获取该锁。这样会导致在某些情况下丧失公平性,因此,如果要遵守公平性,请使用等效的tryLock(0,TimeUnit.SECONDS)方法。此方法还能对中断进行检测。如果当前线程已经持有此锁,则持有计数将增加一,并且该方法返回true。如果该锁由另一个线程持有,则此方法将立即返回值 false。如果该锁被free并且已由当前线程获取,或者锁已经由当前线程持有,否则将返回false。

    public boolean tryLock( ) {
        return sync.tryWriteLock();
    }
    
    4.2.1.4 tryLock(long timeout, TimeUnit unit)

    如果在给定的等待时间内,其他的线程未持有该锁,且当前线程未被打断。则获得该锁。如果读锁或者写锁都未被另外一个线程持有,则获取该写入锁,并且立即返回true,将写锁的计数器保持为1。如果已将此锁设置为公平锁,则如果其他任何线程在等待锁,则不会获取可用的锁,这与tryLock()方法相反,如果你想要一个定时的tryLock公平锁,则将定时和非定时的表达式组合在一起。

    if (lock.tryLock() ||
      lock.tryLock(timeout, unit)) {
    ...
      }}
    

    如果当前线程已经持有此锁,则持有计数增加1,并且该方法返回true。
    如果锁由另外一个线程持有。则出于线程调度目的,当前线程将被禁用,并处于休眠状态,直到发生以下三种情况之一:

    • 当前线程获得写锁;
    • 如果获得写锁,则返回true,并且将写锁计数设置为1。
    • 如果当前线程在进入此方法之前已被标记为中断,获取写锁的过程将被中断。并抛出InterruptedException。
    • 如果结果指定的等待时间,仍然不能获得锁的话,则返回false,如果时间小于0或者等于0,则该方法根本不能等待。

    在此方法中,由于此方法是显式中断,因此,优先响应中断,而不是正常的获取锁,而是优先等待。
    如果该锁是空闲状态,并由当前线程获取,或者写锁已由当前线程持有,则返回tue。如果获取锁之前已经超过了等待时间,则返回false。

    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
    
    4.2.1.5 unlock()

    对锁进行释放。
    如果当前线程是此锁的持有者,则保留计数将减少。如果保持计数现在为零,则释放锁定。如果当前线程不是此锁的持有者,则将引发{@link IllegalMonitorStateException}。

    public void unlock() {
        sync.release(1);
    }
    
    4.2.1.6 newCondition()

    返回与此lock一起使用的Condition实例,返回的Condition实例类似于Object的wait、notify、notifyAll方法。
    如果在调用任何Condition方法的时候未获得写锁,则会返回IllegalMonitorStateException。读锁独立于写锁而持有,因此不会被检查或受影响。但是,当当前线程也已获取读锁时,调用条件等待方法本质上总是错误,因为其他可能解除阻塞它的线程不会被调用。能够获取写锁。
    调用条件{@linkplain Condition#await()等待方法}时,写锁定被释放,并且在返回之前,将重新获取写锁定,并将锁定保持计数恢复到调用该方法时的状态。
    如果一个线程状态是interrupted,此时将抛出InterruptedException。这个线程的中断状态将被清除。
    等待线程按照FIFO顺序。
    从等待方法返回的线程的锁重新获取顺序与最初获取锁的线程的顺序相同(默认情况下未指定),但是对于公平锁,优先使用那些一直在等待锁时间最长的线程。

    public Condition newCondition() {
        return sync.newCondition();
    }
    

    5.方法列表

    ReentrantReadWriteLock支持的方法如下表:

    方法 说明
    isFair() 返回当前ReentrantReadWriteLock的公平性,是否为公平锁,或者非公平锁。
    getOwner() 返回拥有写锁的线程,如果没有任何线程获得写锁,则返回null,当非所有者线程调用此方法的时候,返回值反映当前锁状态的最大近似值。如:有线程尝试获取锁,但是所有者还没有获取完成,因此会在这个时候返回null,设计此方法是为了构造提供更广泛的锁监视功能。
    getReadLockCount() 查询此锁用用的读锁的数量,此方法设计用于监视系统状态,而不用于同步控制。
    isWriteLocked() 查询写锁是否由任何线程持有。此方法设计用于监视系统状态,而不用于同步控制。
    isWriteLockedByCurrentThread() 查询写锁是否由当前线程持有
    getWriteHoldCount() 查询当前线程对该锁持有的重入写入次数。对于未与解锁动作匹配的每个锁定动作,编写器线程均拥有一个锁。
    getReadHoldCount() 查询当前线程对该锁持有的可重入读取次数。对于与解锁操作不匹配的每个锁定操作,读取器线程均拥有一个锁定保持。
    getQueuedWriterThreads() 返回一个包含可能正在等待获取写锁的线程的集合。由于实际的线程集在构造此结果时可能会动态变化,因此返回的集合只是尽力而为的估计。返回的集合的元素没有特定的顺序。设计此方法是为了便于构造提供更广泛的锁监视功能的子类。
    getQueuedReaderThreads() 返回一个包含可能正在等待获取读锁的线程的集合。由于实际的线程集在构造此结果时可能会动态变化,因此返回的集合只是尽力而为的估计。返回的集合的元素没有特定的顺序。设计此方法是为了便于构造提供更广泛的锁监视功能的子类。
    hasQueuedThreads() 查询是否有任何线程正在等待获取读或写锁。请注意,由于取消可能随时发生,因此{@code true}返回值不能保证任何其他线程都将获得锁。此方法主要设计用于监视系统状态。
    hasQueuedThread(Thread thread) 查询给定线程是否正在等待获取读取或写入锁定。请注意,由于取消可能随时发生,因此返回{@code true}并不能保证该线程将获得锁。此方法主要设计用于监视系统状态。
    getQueueLength() 返回等待获取读或写锁的线程数的估计值。该值只是一个估计值,因为在此方法遍历内部数据结构时,线程数可能会动态变化。此方法设计用于监视系统状态,而不用于同步控制。
    getQueuedThreads() 返回一个包含线程的集合,这些线程可能正在等待获取读或写锁。由于实际的线程集在构造此结果时可能会动态变化,因此返回的集合只是尽力而为的估计。返回的集合的元素没有特定的顺序。设计此方法是为了便于构造子类,以提供更广泛的监视功能。
    hasWaiters(Condition condition) 查询是否有任何线程在等待与写锁关联的给定条件。请注意,由于超时和中断可能随时发生,因此{@code true}返回并不保证将来的{@code signal}会唤醒任何线程。此方法主要设计用于监视系统状态。
    getWaitQueueLength(Condition condition) 返回在与写锁关联的给定条件下等待的线程数的估计值。请注意,由于超时和中断可能随时发生,因此估算值仅用作实际侍者数的上限。此方法设计用于监视系统状态,而不用于同步控制。
    getWaitingThreads(Condition condition) 返回一个包含可能正在等待与写锁关联的给定条件的线程的集合。由于实际的线程集在构造此结果时可能会动态变化,因此返回的集合只是尽力而为的估计。返回的集合的元素没有特定的顺序。设计此方法是为了便于构造提供更广泛的状态监视工具的子类。

    6.读/写锁的获取和释放过程

    6.1 读锁的获取过程

    读锁的lock方法,将使用AQS的共享模式:

    public void lock() {
        sync.acquireShared(1);
    }
    

    在这个模式中,首先调用tryAcquireShared,如果结果小于0,那么执行doAcquireShared。将调用线程加入AQS的等待队列,并将线程park。

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
    

    而上述过程的关键在于tryAcquireShared(1)方法。在学习AQS的过程中介绍到,这个方法是个抽象方法,将有各实现类自由实现。
    我们来看看在ReentrantReadWriteLock中的实现过程:

    protected final int tryAcquireShared(int unused) {
      /*
       *推演过程:
       * 1.如果有其他线程持有写锁,则获取失败,返回-1。
       * 2.否则,该线程符合lock的wrt状态,因此判断该队列是否因为其队列的策略而阻塞。如果没有,尝试通过大小写状态和更新计数器来授予。需要注意的是,step不检查可重入获取,它被推迟到完整版本,以避免在更典型的的不可重入情况下检查和保留计数。
       *如果步骤二失败是因为线程明显不合格,CAS计数失败或者计数饱和,则使用完整的重试循环。
       * 3.
       *
       */
       //取得当前线程
        Thread current = Thread.currentThread();
        //获取state为c
        int c = getState();
        //如果独占锁的状态不为0,且存在所有者。则返回-1
        if (exclusiveCount(c) != 0 &&
            getExclusiveOwnerThread() != current)
            return -1;
        // r为读锁的次数
        int r = sharedCount(c);
        //这个方法是读锁和写锁分别要实现的抽象方法,对读和写锁是否需要阻塞进行判断。
        if (!readerShouldBlock() &&
           //同时读锁的次数小于MAX_COUNT
            r < MAX_COUNT &&
            //比较交换更新高位
            compareAndSetState(c, c + SHARED_UNIT)) {
            //如果r为0 则之前没有线程等待
            if (r == 0) {
                //firstReader指向当前线程
                firstReader = current;
                firstReaderHoldCount = 1;
            //反之,如果firstReader本来就是current,则是重入情况
            } else if (firstReader == current) {
               //增加重入的次数
                firstReaderHoldCount++;
            //反之
            } else {
                //rh为cahcedHoldCounter
                HoldCounter rh = cachedHoldCounter;
                //如果rh为null  更新cachedHoldCounter 
                if (rh == null || rh.tid != getThreadId(current))
                    cachedHoldCounter = rh = readHolds.get();
                //反之,如果rh为0 则重置rh
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
            }
            return 1;
        }
        //如果上述过程无返回,则调用完整版本的获取方法。
        return fullTryAcquireShared(current);
    }
    

    上述过程为tryAcquireShared方法的共享实现,首先,根据state状态,判断独占部分的写线程是否存在,并且写线程如果不是当前线程,则说明已经有线程获得了写锁,那么对于读锁,此时是互斥的,因此直接返回-1。
    反之,那么就只有两种情况,要么当前线程就是写线程,要么写锁空闲。如果是写线程,那么就是重入,需要记录firstReaderHoldCount。如果写锁空闲的情况下,此时就需要计数,判断当前线程是否为第一个读线程。
    可以看到在上述执行过程中,要么读线程需要被阻塞,要么达到上限,要么CAS失败,这三种原因会导致调用fullTryAcquireShared。否则就应该能成功获得读锁。
    fullTryAcquireShared如下:

    final int fullTryAcquireShared(Thread current) {
    //此代码与tryAcquireShared中的代码部分冗余,但总体上更简单,因为它不会使tryAcquireShared与重试和惰性读取保持计数之间的交互复杂化。
    HoldCounter rh = null;
    //死循环
    for (;;) {
        //获得state
        int c = getState();
        //如果独占部分不为0
        if (exclusiveCount(c) != 0) {
            //如果当前线程不为独占线程,直接返回-1,这与前面的方法冗余
            if (getExclusiveOwnerThread() != current)
                return -1;
        //反之,如果读线程需要阻塞
        } else if (readerShouldBlock()) {
           //确保不是读锁重入
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                 //更新计数器
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                if (rh.count == 0)
                    return -1;
            }
        }
        //如果读锁达到了最大值,抛出异常
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        //如果成功更改状态,成功返回
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
    }
    

    fullTryAcquireShared与tryAcquireShared存在很多相似之处,部分代码冗余。
    在上述readerShouldBlock方法,这是公平锁和非公平锁的重要实现方法,如果是公平锁,只要队列中有等待线程,那么这个方法就会返回ture,将当前线程等待。也就是说需要对读线程进行阻塞。对于非公平锁,则根据写锁的状态判断是否需要返回false,如果不阻塞,读线程就可能获得锁。

    6.2 写锁的获取过程

    lock方法:

    public void lock() {
        sync.acquire(1);
    }
    

    同样,需要调用tryAcquire,如果tryAcquire返回为false,由于写锁是独占锁,需要添加到等待队列。

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

    tryAcquire方法源码如下:

    protected final boolean tryAcquire(int acquires) {
        /*
        *过程推演:
        *如果读计数器非0,或者写计数器非0,且所有者不是当前线程,则失败。
        *如果计数饱和,则失败,只有当count已非零的时候才会出现这种情况。
        *如果这个线程是可重入或者公平策略,则锁定,排队,那么更新状态,设置所有者线程。
        *
        */
        //获得当前线程
        Thread current = Thread.currentThread();
        //得到state
        int c = getState();
        //获得写锁的次数
        int w = exclusiveCount(c);
        //如果c不为0 
        if (c != 0) {
            // (Note: if c != 0 and w == 0 then shared count != 0)
            //判断写锁的状态,或者当前线程不是独占线程,则返回false
            if (w == 0 || current != getExclusiveOwnerThread())
                return false;
            //如果写锁个数超过了最大值,则抛出异常
            if (w + exclusiveCount(acquires) > MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
            // Reentrant acquire
            //如果是写锁重入,则返回true
            setState(c + acquires);
            return true;
        }
        //如果当前没有写锁或者读锁,如果写线程应该阻塞或者CAS失败,返回false
        if (writerShouldBlock() ||
            !compareAndSetState(c, c + acquires))
            return false;
        //否则将当前线程置为获得写锁的线程,返回true
        setExclusiveOwnerThread(current);
        return true;
    }
    

    对于写锁,其过程如下:

    • 如果c不为0,则说明可能有写锁和读锁。那么再判断如果只有读锁,则返回false,如果有写锁,则判断是否是当前线程,考虑重入性。如果不是,再返回false。
    • 如果写锁数量超过65535,则抛出异常。否则重入写入。
    • 如果没有读锁或者写锁,如果需要阻塞,或者cas失败,则返回false,否则当前线程获得写锁。

    可以看到writerShouldBlock方法与readShouldBlock方法一样,是公平锁或非公平锁实现的控制方法。再公平锁模式下,如果队列中有线程等待,这个先也需要计入等待队列,当没有锁时,如果使用的非公平模式下的写锁的话,那么返回false,直接通过CAS就可以获得写锁。

    6.3 读锁的释放过程

    读锁释放的时候调用releaseShared。

    public void unlock() {
        sync.releaseShared(1);
    }
    

    实际上也是调用了tryReleaseShared方法。如果tru方法返回为true,则调用doRelease方法。

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
    

    Sync中队tyrReleaseShared的实现如下:

    protected final boolean tryReleaseShared(int unused) {
        //获得当前线程
        Thread current = Thread.currentThread();
        //判断当前线程是否为等待队列中的第一个读线程
        if (firstReader == current) {
            // assert firstReaderHoldCount > 0;
            //如果为第一个读线程,且其计数器为1 ,则直接释放
            if (firstReaderHoldCount == 1)
                firstReader = null;
            //否则计数器减1
            else
                firstReaderHoldCount--;
        } else {
           //反之 hold计数器减1
            HoldCounter rh = cachedHoldCounter;
            //如果读计数器为空,且线程不为当前线程
            if (rh == null || rh.tid != getThreadId(current))
                rh = readHolds.get();
            int count = rh.count;
            if (count <= 1) {
                readHolds.remove();
                if (count <= 0)
                    throw unmatchedUnlockException();
            }
            --rh.count;
        }
        //死循环
        for (;;) {
            int c = getState();
            //释放锁
            int nextc = c - SHARED_UNIT;
            //如果CAS更新状态成功,返回读锁是否等于0;失败的话,则重试
            if (compareAndSetState(c, nextc))
                // Releasing the read lock has no effect on readers,
                // but it may allow waiting writers to proceed if
                // both read and write locks are now free.
                return nextc == 0;
        }
    }
    

    通过上述代码可以看出,释放锁首先要更新firstReader,或HoldCounter。前者用于写锁之后的等待队列,而后者用于重入。之后进入死循环,更新AQS状态,一旦成功,则返回。
    读锁的释放队读线程没有影响,但是可能会使得等待的写线程唤醒。

    6.3 写锁的释放过程

    写锁的释放方法:

    public void unlock() {
        sync.release(1);
    }
    

    同样,需要使用独占模式的trytelease方法。

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

    通过tryRelease方法,尝试队锁进行释放。
    一旦锁释放成功,如果等待队列中有线程等待,就将调用unparkSuccessor将后续的等待线程唤醒。

    protected final boolean tryRelease(int releases) {
        //如果没有线程持有写锁,但是仍要释放,抛出异常
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        int nextc = getState() - releases;
        boolean free = exclusiveCount(nextc) == 0;
        //如果没有写锁了,那么将AQS的线程置为null
        if (free)
            setExclusiveOwnerThread(null);
        //更新状态
        setState(nextc);
        return free;
    }
    

    从上面可以看出,如果没有线程持有写锁,如果还要释放,那么会抛出异常。
    得到解锁之后的状态,如果没有写锁,那么AQS的线程设置为null。
    AQS状态的更新不会关心释放设置为null。

    8. 总结

    本文对ReentrantReadWriteLock的源码进行了详细分析,可以看到,ReentrantReadLock几乎揽括了并发操作的大部分关键知识点:

    • 1.公平锁与非公平锁的实现。
    • 2.同时将读锁和写锁对AQS的共享和独占模式分别进行了实现。
    • 3.计数器还使用了ThreadLocal。

    对于读写锁的获取而言,如果当前没有写锁或者读锁,每一个获取锁的线程都会成功。如果读锁已存在,那么此时获取写锁失败,获取读锁有可能成功也有可能失败。如果当前以有写锁,则此时在此获取读锁或者写锁,只有此前的读锁的持有者能获取成功。
    对于读写锁的释放而言,如果当前写锁被占有,只有写锁的计数器下降到0的时候才被认为释放成功。否则失败,因为只要写锁存在,除了持有写锁的线程之外,其他线程总是会被阻塞。如果读锁被占有,那么只有写锁的计数器为0的时候才会被认为释放成功,因为一旦写锁存在,其他的线程都无法获得读锁。

    相关文章

      网友评论

          本文标题:多线程基础(十八):ReentrantReadWriteLock

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