Java 并发之 ReentrantReadWriteLock 深入分析

前言

线程并发系列文章:

Java 线程基础
Java “优雅”地中断线程
Java 线程状态
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有误
Java Unsafe/CAS/LockSupport 应用与原理
Java 并发"锁"的本质(一步步实现锁)
Java Synchronized实现互斥之应用与源码初探
Java 对象头分析与使用(Synchronized相关)
Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程
Java Synchronized 重量级锁原理深入剖析上(互斥篇)
Java Synchronized 重量级锁原理深入剖析下(同步篇)
Java并发之 AQS 深入解析(上)
Java并发之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 详解
Java 并发之 ReentrantLock 深入分析(与Synchronized区别)
Java 并发之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(应用篇)
最详细的图文解析Java各种锁(终极篇)

上篇文章分析了AQS的实际应用之一:ReentrantLock 的实现。ReentrantLock 和synchronized 都是独占锁,而AQS还支持共享锁,本篇就来分析AQS 共享锁的实际应用。
通过本篇文章,你将了解到:

1、共享锁、独享锁区别
2、读锁的实现原理
3、写锁的实现原理
4、读写锁 tryLock 原理
5、读写锁的应用

1、共享锁、独享锁区别

基本差别

共享锁、独占锁是在AQS里实现的,核心是"state"的值:


Java 并发之 ReentrantReadWriteLock 深入分析_第1张图片
image.png

如上图,对于共享锁来说,允许多个线程对state进行有效修改。

读写锁的引入

根据上面的图,state 同时只能表示一种锁,要么独占锁,要么共享锁。而在实际的应用场景里经常会碰到多个线程读,多个线程写的情况,此时为了能够协同读、写线程,需要将state改造。
先来看AQS state 定义:

#AbstractQueuedSynchronizer.java
private volatile int state;

可以看出是int 类型的(当然也有long 类型的,在AbstractQueuedLongSynchronizer.java 里,本文以int 为例)

Java 并发之 ReentrantReadWriteLock 深入分析_第2张图片
image.png

state 被分为两部分,低16位表示写锁(独占锁),高16位表示读锁(共享锁),这样一个32位的state 就可以同时表示共享锁和独占锁了。

2、读锁的实现原理

ReentrantReadWriteLock 的构造

ReentrantReadWriteLock 并没有像ReentrantLock一样直接实现Lock 接口,而是内部分别持有ReadLock、WriteLock类型的成员变量,两者均实现了Lock 接口。

#ReentrantReadWriteLock.java
    public ReentrantReadWriteLock() {
        //默认非公平锁
        this(false);
    }

    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        //构造读锁
        readerLock = new ReadLock(this);
        //构造写锁
        writerLock = new WriteLock(this);
    }

ReentrantReadWriteLock 默认实现非公平锁,读锁、写锁支持非公平锁和公平锁。
读写锁构造之后,将锁暴露出来给外部使用:

#ReentrantReadWriteLock.java
    //获取写锁对象
    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    //获取读锁对象
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

获取锁

在ReentrantLock 分析独占锁时有如下图:


Java 并发之 ReentrantReadWriteLock 深入分析_第3张图片
image.png

与独占锁类似,AQS虽然已经实现了共享锁的基本逻辑,但是真正获取锁、释放锁的操作还是需要子类实现,共享锁需要实现方法:

tryAcquireShared & tryReleaseShared

来看看获取锁的过程:

#ReentrantReadWriteLock.ReadLock
    public void lock() {
            //共享锁
            sync.acquireShared(1);
        }

#AbstractQueuedSynchronizer.java
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            //doAcquireShared 在AQS里实现
            doAcquireShared(arg);
    }    

重点是tryAcquireShared(xx):

#ReentrantReadWriteLock.java
        protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            //获取同步状态
            int c = getState();
            //此处exclusiveCount作用是取state 低16位,若是不等于0,说明有线程占有了写锁
            //若是有线程占有了写锁,而这个线程不是当前线程,则直接退出------------>(1)
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            //获取state 高16位,若是大于0,说明有线程占有了读锁
            int r = sharedCount(c);
            //当前线程是否应该阻塞
            if (!readerShouldBlock() &&//------------>(2)
                r < MAX_COUNT &&//若是不该阻塞,则尝试CAS修改state高16位的值
                compareAndSetState(c, c + SHARED_UNIT)) {
                //--------记录线程/重入次数----------->(3)
                //修改state 成功,说明成功占有了读锁
                if (r == 0) {
                    //记录第一个占有读锁的线程
                    firstReader = current;
                    //占有次数为1
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    //第一个占有读锁的线程重入了该锁
                    firstReaderHoldCount++;
                } else {
                    //是其它线程占有锁
                    //取出缓存的HoldCounter
                    HoldCounter rh = cachedHoldCounter;
                    //若是缓存为空,或是缓存存储的不是当前的线程
                    if (rh == null || rh.tid != getThreadId(current))
                        //从threadLocal里获取
                        //readHolds 为ThreadLocalHoldCounter 类型,继承自ThreadLocal
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        //说明cachedHoldCounter 已经被移出threadLocal,
                        //重新加入即可------------>(4)
                        readHolds.set(rh);
                    //记录重入次数
                    rh.count++;
                    //--------记录线程/重入次数-----------
                }
                return 1;
            }
            //------------>(5)
            return fullTryAcquireShared(current);
        }

以上是获取读锁的核心代码,标注了5个重点,分别来分析。
(1)
此处表明了一个信息:

若是当前线程已经获取了写锁,那么它可以继续尝试获得读锁。
当它把写锁释放后,只剩读锁了。这个过程可以理解为锁的降级。

(2)
线程能否有机会获取读锁,还需要经过两个判断:

1、判定readerShouldBlock()。
2、判定读锁个数用完了没,阈值是2^16-1。

而读锁公平与否就体现在readerShouldBlock()的实现上。

先来看非公平读锁:

#ReentrantReadWriteLock.java
        final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();
        }

#AbstractQueuedSynchronizer.java
       final boolean apparentlyFirstQueuedIsExclusive() {
        //判断等待队列里的第二个节点是否在等待写锁
        Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
    }

若等待队列里的第二个节点是在等待写锁,那么此时不能去获取读锁。
这与ReentrantLock不一样,ReentrantLock 非公平锁的实现是不管等待队列里有没有节点,都会去尝试获取锁。

再来看公平读锁

#ReentrantReadWriteLock.java
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }

判断队列里是否有更早于当前线程排队的节点,该方法在上篇分析ReentrantLock 时有深入分析,此处不再赘述。

(3)
这部分代码看起来多,实际上就是为了记录重入次数以及为了效率考虑引入了一些缓存。
考虑到有可能始终只有一个线程获取读锁,因此定义了两个变量还记录重入次数:

#ReentrantReadWriteLock.java
        //记录第一个获取读锁的线程
        private transient Thread firstReader = null;
        //第一个获取读锁的线程获取读锁的个数
        private transient int firstReaderHoldCount;

再考虑到有多个线程获取锁,它们也需要记录获取锁的个数,与线程绑定的数据我们想到了ThreadLocal,于是定义了:

private transient ThreadLocalHoldCounter readHolds;

来记录HoldCounter(存储获取锁的个数及绑定的线程id)。
最后为了不用每次都去ThreadLocal里查询数据,再定义了变量来缓存HoldCounter:

#ReentrantReadWriteLock.java
private transient HoldCounter cachedHoldCounter;

(4)
cachedHoldCounter.count == 0,是在tryReleaseShared(xx)里操作的,并且判断当线程已经彻底释放了读锁后,将HoldCounter 从ThreadLocal里移除,因此此处需要加回来。

(5)
走到这一步,说明之前获取锁的操作失败了,原因有三点:

1、readerShouldBlock() == true。
2、r >= MAX_COUNT。
3、中途有其它线程修改了state。

fullTryAcquireShared(xx)与tryAcquireShared(xx)很类似,目的就是为了获取锁。
针对第三点,fullTryAcquireShared(xx)里有个死循环,不断获取state值,若是符合1、2点,则退出循环,否则尝试CAS修改state,若是失败,则继续循环获取state值。

小结一下:

1、fullTryAcquireShared(xx) 获取锁失败返回-1,接下来的处理逻辑流转到AQS里,线程可能会被挂起。
2、fullTryAcquireShared(xx) 获取锁成功则返回1。

释放锁

释放锁的逻辑比较简单:

#ReentrantReadWriteLock.ReadLock
    public void lock() {
            sync.acquireShared(1);
        }
#AbstractQueuedSynchronizer.java
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            //在AQS里实现
            doReleaseShared();
            return true;
        }
        return false;
    }

重点是tryReleaseShared(xx):

#ReentrantReadWriteLock.java
        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            //当前线程是之前第一个获取读锁的线程
            if (firstReader == current) {
                if (firstReaderHoldCount == 1)
                    //彻底释放完了,置空
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                //先从缓存里取
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    //取不到,则需要从ThreadLocal里取
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    //若是当前线程不再占有锁,则清除对应的ThreadLocal变量
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            for (;;) {
                //修改state
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    //若是state值变为0,说明读锁、写锁都释放完了
                    return nextc == 0;
            }
        }

此处需要注意的是:
tryReleaseShared(xx)释放读锁时候,若是没有完全释放读锁、写锁,那么将会返回false。
而在AQS里释放共享锁流程如下:

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

也就是说此种情况下,doReleaseShared() 将不会被调用,也就不会唤醒同步队列里的节点。
这么做的原因是:

若只释放完读锁,还剩写锁被占用。而因为写锁是独占锁,其它线程无法获取锁,那么即使唤醒了它们也没有用。

3、写锁的实现原理

获取锁

写锁是独占锁,因此重点关注tryAcquire(xx):

#ReentrantReadWriteLock.java
        protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            //获取同步状态
            int c = getState();
            //获取当前写锁个数
            int w = exclusiveCount(c);
            if (c != 0) {
                //1、若是w==0,而c!= 0,说明有线程占有了读锁,不能再获取写锁了
                //2、若是写锁被占用,但是不是当前线程,则不能再获取写锁了
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //锁个数超限了
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");

                //走到此处,说明重入,直接设置,同一时刻只有一个线程能走到这
                setState(c + acquires);
                return true;
            }
            //若c==0,此时读锁、写锁都没线程占用
            //判断线程是否应该被阻塞,否则尝试获取写锁------->(1)
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            //独占锁需要关联线程
            setExclusiveOwnerThread(current);
            return true;
        }

来看看writerShouldBlock(),写锁公平/非公平就在此处实现的。

先来看非公平写锁:

#ReentrantReadWriteLock.java
        final boolean writerShouldBlock() {
            //不阻塞
            return false; // writers can always barge
        }

非公平写锁不应该阻塞。

再来看公平写锁:

#ReentrantReadWriteLock.java
        final boolean writerShouldBlock() {
            //判断队列是否有有效节点等待
            return hasQueuedPredecessors();
        }

和公平读锁一样的判断条件。

小结

1、读锁/写锁 已被其它线程占用,那么新来的线程将无法获取写锁。
2、写锁可重入。

释放锁

释放锁重点关注tryRelease(xx):

##ReentrantReadWriteLock.java
        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;
        }

若tryRelease(xx)返回true,则AQS里会唤醒等待队列的线程。

4、读写锁 tryLock 原理

读锁tryLock

#ReentrantReadWriteLock.java
        public boolean tryLock() {
            return sync.tryReadLock();
        }

        final boolean tryReadLock() {
            Thread current = Thread.currentThread();
            for (;;) {
            //for 循环为了检测最新的state
                int c = getState();
                if (exclusiveCount(c) != 0 &&
                    getExclusiveOwnerThread() != current)
                    return false;
                int r = sharedCount(c);
                if (r == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                //记录次数
                    if (r == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        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 true;
                }
            }
        }

可以看出tryReadLock(xx)里: 只要不是别的线程占有写锁并且读锁个数没超出限制,那么它将一直尝试获取读锁,直到得到为止。

写锁tryLock

        public boolean tryLock() {
            return sync.tryWriteLock();
        }
        final boolean tryWriteLock() {
            Thread current = Thread.currentThread();
            int c = getState();
            if (c != 0) {
                int w = exclusiveCount(c);
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
            }
            if (!compareAndSetState(c, c + 1))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

写锁只尝试一次CAS,失败就返回。
最终,用图表示读锁、写锁实现的功能:


Java 并发之 ReentrantReadWriteLock 深入分析_第4张图片
image.png

读锁与写锁关系:


Java 并发之 ReentrantReadWriteLock 深入分析_第5张图片
image.png

5、读写锁的应用

分析完原理,来看看简单应用。

public class TestThread {

    static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
    static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

    public static void main(String args[]) {
        //读
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                    try {
                        System.out.println("thread " + threadName + " acquire read lock");
                        readLock.lock();
                        System.out.println("thread " + threadName + " read locking");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        readLock.unlock();
                        System.out.println("thread " + threadName + " release read lock remain read count:" + readWriteLock.getReadLockCount());
                    }
                }
            }, "" + i).start();
        }

        //写
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                    try {
                        System.out.println("thread " + threadName + " acquire write lock");
                        writeLock.lock();
                        System.out.println("thread " + threadName + " write locking");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        writeLock.unlock();
                        System.out.println("thread " + threadName + " release write lock remain write count:" + readWriteLock.getWriteHoldCount());
                    }
                }
            }, "" + i).start();
        }
    }
}

10个线程获取读锁,10个线程获取写锁。
读写锁应用场景:

  • ReentrantReadWriteLock 适用于读多写少的场景,提高多线程读的效率、吞吐量。

同一线程读锁、写锁关系:

public class TestThread {

    static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
    static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

    public static void main(String args[]) {
//        new TestThread().testReadWriteLock();------>1、先读锁,后写锁
//        new TestThread().testWriteReadLock();------>2、先写锁、后读锁
    }

    private void testReadWriteLock() {
        System.out.println("before read lock");
        readLock.lock();
        System.out.println("before write lock");
        writeLock.lock();
        System.out.println("after write lock");
    }

    private void testWriteReadLock() {
        System.out.println("before write lock");
        writeLock.lock();
        System.out.println("before read lock");
        readLock.lock();
        System.out.println("after read lock");
    }
}

分别打开1、2 注释,发现:

1、先获取读锁,再获取写锁,则线程在写锁处挂起。
2、先获取写锁,再获取读锁,则都能正常获取锁。
这与我们上述的理论分析一致。

下篇将会分析Semaphore、CountDownLatch、 CyclicBarrier原理及其应用。

本文基于jdk1.8。

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android/Java

1、Android各种Context的前世今生
2、Android DecorView 一窥全貌(上)
3、Android DecorView 一窥全貌(下)
4、Window/WindowManager 不可不知之事
5、View Measure/Layout/Draw 真明白了
6、Android事件分发全套服务
7、Android invalidate/postInvalidate/requestLayout 彻底厘清
8、Android Window 如何确定大小/onMeasure()多次执行原因
9、Android事件驱动Handler-Message-Looper解析
10、Android 键盘一招搞定
11、Android 各种坐标彻底明了
12、Android Activity/Window/View 的background
13、Android IPC 之Service 还可以这么理解
14、Android IPC 之Binder基础
15、Android IPC 之Binder应用
16、Android IPC 之AIDL应用(上)
17、Android IPC 之AIDL应用(下)
18、Android IPC 之Messenger 原理及应用
19、Android IPC 之获取服务(IBinder)
20、Android 存储基础
21、Android 10、11 存储完全适配(上)
22、Android 10、11 存储完全适配(下)
23、Java 并发系列不再疑惑

你可能感兴趣的:(Java 并发之 ReentrantReadWriteLock 深入分析)