读写锁ReentrantReadWriteLock源代码浅析

Posted gocode

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了读写锁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;
        }

 

以上是关于读写锁ReentrantReadWriteLock源代码浅析的主要内容,如果未能解决你的问题,请参考以下文章

[图解Java]读写锁ReentrantReadWriteLock

JUC中的读写锁(ReentrantReadWriteLock)

读写锁 ReentrantReadWriteLock

ReentrantReadWriteLock读写锁的使用

AQS系列- ReentrantReadWriteLock读写锁的加锁

ReentrantReadWriteLock读写锁