并发编程 — ReadWriteLock 读写锁实现原理详解

目录

一、概述

二、ReentrantReadWriteLock 实现原理

1、ReentrantReadWriteLock 类层次结构

2、使用范式

3、读写锁的基本实现原理

4、写锁的获取与释放

5、读锁的获取与释放

三、总结


一、概述

读写锁与排他锁(独占锁)不同的是,读写锁在同一时刻可以允许多个读线程方法,但是在写线程访问时,所有的读线程和其它写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,是的并发性相比一般的排它锁有了很大的提升。

一般情况下,读写锁的性能都会比排它锁好,因为在多数场景读是多于写的。在读多写少的场景中,读写锁提供比排它锁更好的并发性和吞吐量。Java并发包提供读写锁的实现是ReentrantReadWriteLock。

二、ReentrantReadWriteLock 实现原理

1、ReentrantReadWriteLock 类层次结构

在介绍 ReentrantReadWriteLock 之前我们先看看其类的实现层次结构:

通过类的实现继承关系我们可知 ReentrantReadWriteLock 实现了 ReadWriteLock 接口,ReadWriteLock  接口也很简单其内部主要提供了两个方法,分别返回读锁和写锁如下所示:

public interface ReadWriteLock {
    //获取读锁
    Lock readLock();

    //获取写锁
    Lock writeLock();
}

2、使用范式

读写锁的使用方式如下所示:

        Lock readLock = readWriteLock.readLock();
        readLock.lock();
        try {
           // TODO
        } finally {
            readLock.unlock();
        }
        Lock writeLock = readWriteLock.writeLock();
        writeLock.lock();
        try {
            // TODO
        } finally {
            writeLock.unlock();
        }

也就是说,当使用ReadWriteLock 的时候,并不是直接使用,而是获得其内部的读锁和写锁,然后分别调用lock/unlock。

3、读写锁的基本实现原理

从表面来看,ReadLock和WriteLock是两把锁,实际上它只是同一把锁的两个视图而已。什么叫两个视图呢?可以理解为是一把锁,线程分成两类:读线程和写线程。读线程和读线程之间不互斥(可以同时拿到这把锁),读线程和写线程互斥,写线程和写线程也互斥。

从ReentrantReadWriteLock 的构造方法中可知,readerLock 和 writerLock 实际共用同一个 sync 对象。sync 对象同互斥锁一样,分为非公平和公平两种策略,并继承自AQS。

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

同互斥锁一样,读写锁也是用 state 变量来表示锁状态的。只是 state 变量在这里的含义和互斥锁完全不同。在 ReentrantLock 中 同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态上维护多个读线程和一个写线程的状态,那针对一个 int 类型的同步状态它是怎么做到的呢?在读写锁中是通过“按位切割”的方式实现的,也就是把这个 int类型的变量切分为两部分,高16位表示读,低16位表示写。划分如下所示:

读写状态

这个地方的设计很巧妙,为什么要把一个 int 类型变量拆成两半,而不是用两个int型变量分别表示读锁和写锁的状态呢?这是因为无法用一次CAS 同时操作两个int变量,所以用了一个int型的高16位和低16位分别表示读锁和写锁的状态。当state=0时,说明既没有线程持有读锁,也没有线程持有写锁;当state!=0时,要么有线程持有读锁,要么有线程持有写锁,两者不能同时成立,因为读和写互斥。这时再进一步通过sharedCount(state)和exclusiveCount(state)判断到底是读线程还是写线程持有了该锁。

那么读写锁又是如何快速确定读和写各自的状态的呢?答案是通过位运算。假设当前同步状态值为 V,写状态等于 V & 0x0000FFFF(将高16位全部抹去),读状态等于 V >>> 16 (无符号补0右移16位)。如下源码所示:

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;

        // 获取读锁数量
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

        // 获取写锁被重入的次数
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
}

4、写锁的获取与释放

4.1、获取锁

写锁是一个支持重入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁或者写锁已被其他线程获取,则当前线程进入等待状态,代码如所示:

// 读锁实现类
public static class WriteLock implements Lock, java.io.Serializable {
       
        private final Sync sync;
   
        protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
        
        public void lock() {
            sync.acquire(1); //获取写锁,为排它锁
        }
}


// Sync 实现的 AbstractQueuedSynchronizer 中获取独占锁的方法
        protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState(); // 获取当前同步状态
            int w = exclusiveCount(c); // 获取写锁重入数量
            if (c != 0) { // 如果当前同步状态不为 0,表示锁(读锁或写锁)已被占用

                // 如果 w == 0 说明存在读锁,直接返回,为了保证写对读可见,当前写线程必须阻塞。
                // 如果 w != 0 说明存在写锁,判断当前线程不是已获取写锁的线程,获取锁失败,当前线程被阻塞
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;

                if (w + exclusiveCount(acquires) > MAX_COUNT) // 如果低 16位满了表示超过了获取锁数量的最大值抛出异常
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() || // 此方法是判断前面是否存在等待的写线程
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current); //如果抢锁成功,把持有锁的线程更新为当前线程
            return true;
        }

上面的代码分析如下:

  1. if(c!=0) and w == 0,说明当前一定是读线程拿着锁,写锁一定拿不到,返回false。
  2. if(c!=0) and w != 0,说明当前一定是写线程拿着锁,执行current!=getExclusiveOwnerThread()的判断,发现ownerThread不是自己,返回false。
  3. c!=0,w!=0,且current=getExclusiveOwnerThread(),才会走到if(w+exclusiveCount(acquires)> MAX_COUNT)。判断重入次数,重入次数超过最大值,抛出异常。因为是用state的低16位保存写锁重入次数的,所以MAX_COUNT是216。如果超出这个值,会写到读锁的高16位上。为了避免这种情形,这里做了一个检测。当然,一般不可能重入这么多次。
  4. if(c==0),说明当前既没有读线程,也没有写线程持有该锁。可以通过CAS操作开抢了。

 公平实现和非公平实现几乎一模一样,只是 writerShouldBlock() 分别被FairSync 和NonfairSync实现,公平锁会判断是否有在等待的写锁,而非公平锁则会直接抢锁。

4.2、释放锁

写锁的释放与 Reentrant 的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续的读线程可见。代码如下所示:

        protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively()) //如果当前线程没有获取锁则抛出异常
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases; // 是当前写状态减1.
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc); // 因为写锁为排它锁,不会存在其他线程占有写锁或读锁,所有不用使用 CAS操作
            return free;
        }

关于写锁的具体流程可以参考《ReentrantLock 原理详解

5、读锁的获取与释放

读锁是一个支持重入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问时,读锁总会被成功的获取,而所做的只是增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入阻塞等待状态。读状态是所有线程获取读锁的总和,而每个线程各自获取读锁的次数保存在ThreadLocal中,由线程自身维护。

5.1、获取锁

在tryAcquireShared() 方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或写锁未被占用,则当前线程增加读状态,成功获取锁。代码如下所示:

protected final int tryAcquireShared(int unused) {

            Thread current = Thread.currentThread();
            int c = getState();
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current) // 如果写锁被已被占有,并且占有写锁的线程非当前线程
                                                      // 则不允许获取读锁,需要阻塞等待。
                return -1;
            int r = sharedCount(c); // 获取读锁的状态
            if (!readerShouldBlock() && // 判断当前线程是否需要阻塞等待。
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                if (r == 0) { // 如果读锁未被获取过,则更新第一个读线程为当前线程
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) { // 如果第一个读线程为当前线程则数量加1
                    firstReaderHoldCount++;
                } else {
                    // 获取读锁的线程重入的次数是保存在 ThreadLocal 中的有线程本身保存
                    // 着自己获取锁的次数
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current); // 如果上面拿锁失败,则进行自旋不停的拿锁
        }

5.2、释放锁

因为读锁是 共享锁,所以在释放读锁时是通过 CAS + 自旋 的方式不停的更改锁状态直到更新成功。如下所示:

        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                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 (;;) { // 通过自旋 + CAS 的方式更新读锁状态
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

6、锁降级

锁降级是指写锁可以降级为读锁。如果当前线程拥有写锁,在不释放写锁的情况下是可以在获取读锁的,完后在释放(先前获取)的写锁。ReentrantReadWriteLock 是支持锁降级,但不支持锁升级。

三、总结

ReadWriteLock 接口表示存在两个锁:一个读锁和一个写锁但在基于 AQS 实现的 ReentrantReadWriteLock 中,单个AQS 子类将同时管理读取加锁和写入加锁。ReentrantReadWriteLock 是通过 state 同步状态的 高16位表示读锁状态,低 16 位表示写锁状态。在读锁操作上使用了共享的获取和释放方法,在写锁操作上才用了独占的获取和释放方法。

参考文献:

《Java并发实现原理:JDK源码剖析》

《Java并发编程的艺术》

《Java并发编程实战》

猜你喜欢

转载自blog.csdn.net/small_love/article/details/111406016