读写锁ReentrantReadWriteLock源代码浅析

1.简介

并发中常用的ReentrantLock,是一种典型的排他锁,这类锁在同一时刻只允许一个线程进行访问,实际上将并行操作变成了串行操作。在并发量大的业务中,其整体效率、吞吐量不能满足实现的需要。而且实际的业务中一般情况是读多于写,多个线程读操作不会改变已经有的数据,不会有数据的一致性问题,而一个写操作就会改变数据,其他的的读操作就可能读到过期的数据。读写锁正是为了这种业务需求而产生的,读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

读写锁同时支持公平锁和非公平锁,默认实现是非公平锁,非公平锁的吞吐量高于公平锁。

读写锁也支持重入,读锁和写锁的最大重入次数是65535次,这是由int类型本身所能表达的范围区间所决定的。

读写锁能够实现锁降级,它按照先获取写锁、获取读锁再释放写锁的次序执行锁操作,而且写锁能够降级为读锁。

2.ReentrantReadWriteLock的类结构

从UML图可以看出ReentrantReadWriteLock中有多个内部类,这些内部类与ReentrantLock有些类似,都有Sync、NofairSync、FairSync,前者Sync继承于AQS,前者Sync是NofairSync、FairSync的父类。

与 ReentrantLock相比,虽然这3个静态内部类名字相同,但内部却有差异。ReentrantLock是排他锁,又可以说是一种写锁,这三个静态内部类只重写了AQS中的tryAcquire(int) 、tryRelease(int)这两个排他锁相关方法。而ReentrantReadWriteLock是读写锁,要实现排他锁和共享锁这两种锁,Sync、NofairSync、Fair不仅重写SynctryAcquire(int) 、tryRelease(int)方法,另外还重写了AQS中的tryAcquireShared(int)、 tryReleaseShared(int)这两个共享锁相关方法。

ReadLock是表示读锁的静态内部类,它主要委托NofairSync/FairSync中的共享锁相关方法实现的,如"void acquire(int)" "boolean release(int)"等(这两个方法是父类AQS的模板方法,模板方法再去调用自身重写的tryAcquire(int) 、tryRelease(int)方法)。

WriteLock是表示写锁的静态内部类,它主要委托NofairSync/FairSync中的排他锁相关方法实现的,如"void acquireShared(int)" "boolean releaseShared(int)"等。

HoldCounter和ThreadLocalHoldCounter又都是Sync的静态内部类,HoldCounter类的主要作用是记录获一个获取取到共享锁的读线程的重入次数,ThreadLocalHoldCounter主要是为每个获取取到共享锁的读线程单独维护一个HoldCounter。

3.ReadWriteLock的一此方法与示例

ReadWriteLock接口只有两个抽象方法,分别获取读锁和读锁。
public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

另外ReentrantReadWriteLock类还有一些使我们更容易并发编程的一些辅助方法,这些方法都直接委托Sync(或其子类NofairSync/FairSync)去实现。

    public int getReadLockCount() {
        return sync.getReadLockCount();
    }

    public boolean isWriteLocked() {
        return sync.isWriteLocked();
    }

    public boolean isWriteLockedByCurrentThread() {
        return sync.isHeldExclusively();
    }

    public int getWriteHoldCount() {
        return sync.getWriteHoldCount();
    }

    public int getReadHoldCount() {
        return sync.getReadHoldCount();
    }

getReadLockCount()返回成功获取读锁的线程数,即此锁持有的读取锁的数量。。

isWriteLocked()返回写锁是否被某个线程成功获取了。

isWriteLockedByCurrentThread()返回写锁是否被当前线程成功获取了。

getWriteHoldCount()返回当前线程重复获取写锁(重入)的次数。

getReadHoldCount()返加当前线程重复获取读锁(重入)的次数。

4.保存读写状态的设计理念

ReentrantLock使用AQS中的int类型的state成员变量来保存排他锁状态,重入一次使state自增1,每释放一次重入状态state自减1(以前的帖子)。ReentrantReadWriteLock的重入设计理念应该也是基于此。ReentrantLock只需要保存写锁状态,直接使用state成员变量就可以实现,关键在于如何同时保存ReentrantReadWriteLock的读锁状态与写锁状态。其实可以借鉴CPU的标志寄存器的设计理念,将一个变量"按位分割",不同的位范围表示不同的含义。ReentrantReadWriteLock就是将state的高16位表示读状态、低16位表示写状态(int类型4个字节,共32比特位)。

利用位运算操作,可以分别将高16位和低16位的值取出来。

取低16位的写状态,只需要将state的高16位的所有位设为0即可,根据“0和(1或0)进行按位与操作结果都是0、(0或1)和1进行与操作结果均为其本身”的特点,可以使用按位与操作表达式"state&0x0000FFFF"实现,所以在Sync中exculusiveCount()方法体中有"c&EXCLUSIVE_MASK",而EXCLUSIVE_MASK等于0x0000FFFF。

取高16位的读状态,只需要将state的低16位的抹除即可,可将state无符号右移16位,所以Sync中的sharedCount方法体中有代码"c >>> SHARED_SHIFT"。

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

5.写锁的获取与释放

写锁的lock()方法实际调用Sync的父类AQS的acquire(int)方法,acquire(int)是模板方法,acquire(int)的主要逻辑在以前的帖子中分析过,我们重点关注被Sync重写的tryAcuqrie(int)方法,

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

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
tryAcquire的基本逻辑:如果读锁被获取了或写锁被其他线程获取了,那么尝试获取写锁失败。如果在之前当前线程已经获取了写锁,增加重入次数,尝试获取写锁成功。若未有任何读锁、写锁被获取,则进行CAS更新state,若更新成功,尝试获取写锁成功,返回true,若更新失败,尝试获取写锁失败,返回false.
    protected final boolean tryAcquire(int acquires) {
        /*
         * Walkthrough:
         * 1. If read count nonzero or write count nonzero
         *    and owner is a different thread, fail.
         * 2. If count would saturate, fail. (This can only
         *    happen if count is already nonzero.)
         * 3. Otherwise, this thread is eligible for lock if
         *    it is either a reentrant acquire or
         *    queue policy allows it. If so, update state
         *    and set owner.
         */
        Thread current = Thread.currentThread();
        int c = getState();
        int w = exclusiveCount(c);//写锁重入的次数
        if (c != 0) {//当前锁至少持有1个读锁或1个写锁(任何情况下最多只能有一个写锁)
            // (Note: if c != 0 and w == 0 then shared count != 0)
            /**
             *  因为 共享锁重入次数+排他锁重入次数 < state ,即 shareCout+ w<c,而又c>0 && w=0,
             *  那么shareCount>0,当前读锁被某线程获取了,所以这里w=0表示当前读锁被某些线程获取了,
             *  在读锁被获取了的情况下,不能获取写锁(只有在所有读锁、写锁均补充释放才能获取写锁),返回false。
             *
             *  "current != getExclusiveOwnerThread()"为true表明,
             *   前置条件"w==0"不成立,那么w>0,即写锁已经被某线程获取到了,
             *   而"current != getExclusiveOwnerThread()"条件本身又表明,当前线程不是获取到写锁的线程
             *   所以尝试获取写锁失败,返回false
             *
             *   综合起来说,当有读锁被某些线程成功获取或写锁被其他线程成功获取时,
             *   尝试获取写锁失败,返回false。
             */
            if (w == 0 ||  current != getExclusiveOwnerThread())
                return false;
            if (w + exclusiveCount(acquires) > MAX_COUNT) //超出了最大可重入次数,MAX_COUNT=0x0000FFFF
                throw new Error("Maximum lock count exceeded");
            // Reentrant acquire
             //之前写锁已经被当前线程成功获取,重入次数自增
            //尝试获取写锁成功,返回true
            setState(c + acquires);
            return true;
        }

        //getState=0,当前锁不持有任何读锁、写锁

        if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
            return false; //cas更新失败,尝试获取写锁失败,返回false
        //cas更新成功,设置当前线程为独占线程,尝试获取写锁成功,,返回false
        setExclusiveOwnerThread(current);
        return true;
    }

这里与ReentrantLock不同的地方在于多了对读锁是否存在的判断。读锁的读取线程进行读取操作时,它不能主动感知写入操作,如果同时进行读写操作,读入的数据可能是被删除的数据或是过期的数据,读取时不能写入,所以在已有读锁的时候不能再去获取写锁。反过来也是一样,为保证数据的一致性,在有写锁的时候,它要阻塞其他所有的读写操作,不能再去获取任何读锁和写锁。

tryRelease(int)尝试释放锁的基本逻辑和ReentrantLock几乎一样:将重入次数自减,当重入次数为0,将独占线程设为null,返回true,反之返回false.
        protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }

原文地址:https://www.cnblogs.com/gocode/p/12310430.html

时间: 2024-10-12 20:26:39

读写锁ReentrantReadWriteLock源代码浅析的相关文章

读写锁 ReentrantReadWriteLock

一.读写锁 ReadWriteLock概念特点读写锁维护了一对相关的锁,一个用于只读操作,一个用于写入操作.只要没有writer,读取锁可以由多个reader线程同时保持.写入锁是独占的. 互斥锁[ReetrantLock]一次只允许一个线程访问共享数据,哪怕进行的是只读操作:读写锁[ReadWriteLock]允许对共享数据进行更高级别的并发访问:对于写操作,一次只有一个线程(write线程)可以修改共享数据,对于读操作,允许任意数量的线程同时进行读取.writer可以获取读取锁,但reade

[图解Java]读写锁ReentrantReadWriteLock

图解ReentrantReadWriteLock 如果之前使用过读写锁, 那么可以直接看本篇文章. 如果之前未使用过, 那么请配合我的另一篇文章一起看:[源码分析]读写锁ReentrantReadWriteLock 0. demo 我先给出一个demo, 这样大家就可以根据我给的这段代码, 边调试边看源码了. 还是那句话: 注意"My" , 我把ReentrantReadWriteLock类 改名为了 "MyReentrantReadWriteLock"类 , &q

被面试官吊打系列之JUC之 可重入读写锁ReentrantReadWriteLock 之 源码详尽分析

可重入读写锁 ReentrantReadWriteLock 其实基本上模拟了文件的读写锁操作.ReentrantReadWriteLock 和ReentrantLock 的差别还是蛮大的: 但是也有很多的相似之处: ReentrantReadWriteLock 的 writerLock 其实就是相当于ReentrantLock,但是它提供更多的细腻的控制:理解什么是读锁.写锁非常重要,虽然实际工作中区分读写锁这样的细分使用场景比较少. ReentrantReadWriteLock 把锁进行了细化

[源码分析]读写锁ReentrantReadWriteLock

一.简介 读写锁. 读锁之间是共享的. 写锁是独占的. 首先声明一点: 我在分析源码的时候, 把jdk源码复制出来进行中文的注释, 有时还进行编译调试什么的, 为了避免和jdk原生的类混淆, 我在类前面加了"My". 比如把ReentrantLock改名为了MyReentrantLock, 在源码分析的章节里, 我基本不会对源码进行修改, 所以请忽视这个"My"即可. 1. ReentrantReadWriteLock类里的字段 unsafe在这里是用来给TID_O

深入浅出 Java Concurrency (13): 锁机制 part 8 读写锁 (ReentrantReadWriteLock) (1)[转]

从这一节开始介绍锁里面的最后一个工具:读写锁(ReadWriteLock). ReentrantLock 实现了标准的互斥操作,也就是一次只能有一个线程持有锁,也即所谓独占锁的概念.前面的章节中一直在强调这个特点.显然这个特点在一定程度上面减低了吞吐量,实际上独占锁是一种保守的锁策略,在这种情况下任何“读/读”,“写/读”,“写/写”操作都不能同时发生.但是同样需要强调的一个概念是,锁是有一定的开销的,当并发比较大的时候,锁的开销就比较客观了.所以如果可能的话就尽量少用锁,非要用锁的话就尝试看能

深入浅出 Java Concurrency (14): 锁机制 part 9 读写锁 (ReentrantReadWriteLock) (2)[转]

这一节主要是谈谈读写锁的实现. 上一节中提到,ReadWriteLock看起来有两个锁:readLock/writeLock.如果真的是两个锁的话,它们之间又是如何相互影响的呢? 事实上在ReentrantReadWriteLock里锁的实现是靠java.util.concurrent.locks.ReentrantReadWriteLock.Sync完成的.这个类看起来比较眼熟,实际上它是AQS的一个子类,这中类似的结构在CountDownLatch.ReentrantLock.Semapho

多线程之使用读写锁ReentrantReadWriteLock实现缓存系统

简单地缓存系统:当有线程来取数据时,如果该数据存在我的内存中,我就返回数据:如果不存在我的缓存系统中,那么就去查数据库,返回数据的同时保存在我的缓存中. 其中涉及到读写问题:当多个线程执行读操作时(都加读锁),如果有数据返回:如果没有数据时,则让第一个读的线程,进行获取数据,然后进行写操作,这时需要第一个线程先释放掉读锁然后加写锁.第一个写完后,在家读锁,其他线程使用时判断,如果存在该数据,在直接过去读取不用加写锁. API上缓存例子如下: class CachedData { Object d

Java读写锁(ReentrantReadWriteLock)学习

什么是读写锁 平时,我们常见的synchronized和Reentrantlock基本上都是排他锁,这些锁在同一时刻只允许一个线程进行访问,哪怕是读操作.而读写锁是维护了一对锁(一个读锁和一个写锁),通过分离读锁和写锁,使得同一时刻可以允许多个读线程访问,但是在写线程进行访问时,所有的读线程和其他写线程均被阻塞. 读写锁的优点 1. 简化了读写交互场景编程的复杂度: 在常见的开发中,我们经常会定义一个共享的用作缓存的数据结构:比如一个大Map,缓存全部的城市Id和城市name对应关系.这个大Ma

深入浅出 Java Concurrency (13): 锁机制 part 8 读写锁 (ReentrantReadWriteLock) (1)

从这一节开始介绍锁里面的最后一个工具:读写锁(ReadWriteLock). ReentrantLock 实现了标准的互斥操作,也就是一次只能有一个线程持有锁,也即所谓独占锁的概念.前面的章节中一直在强调这个特点.显然这个特点在一定程度上面减低了吞吐量,实际上独占锁是一种保守的锁策略,在这种情况下任何"读/读","写/读","写/写"操作都不能同时发生.但是同样需要强调的一个概念是,锁是有一定的开销的,当并发比较大的时候,锁的开销就比较客观了.所