Java 并发之 ReentrantReadWriteLock 深入分析

526 阅读9分钟

前言

线程并发系列文章:

Java 线程基础
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"的值:

image.png

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

读写锁的引入

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

#AbstractQueuedSynchronizer.java
private volatile int state;

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

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 分析独占锁时有如下图:

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,失败就返回。
最终,用图表示读锁、写锁实现的功能:

image.png

读锁与写锁关系:

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