0
点赞
收藏
分享

微信扫一扫

从源码角度深入了解ReentrantReadWriteLock的基本原理-2

拾光的Shelly 2022-03-27 阅读 79

在这里插入图片描述
神仙姐姐带你撸源码,讲原理。come on!!!!!!在这里插入图片描述

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

1 读锁与写锁

在这里插入图片描述
那么WriteLock与ReadLock对Lock接口具体是如何实现的呢?

2 AQS

AQS(AbstractQueuedSynchronizer)抽象类定义了一套多线程访问共享资源的同步模板,解决了实现同步器时涉及的大量细节问题,能够极大地减少实现工作,用大白话来说,AQS为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定。AQS简化流程图如下:
在这里插入图片描述

3 Sync

AQS为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定,但是WriteLock与ReadLock没有直接去继承AQS。因为WriteLock与ReadLock觉得,自己还要去继承AQS实现一些两者可以公用的抽象函数,不仅麻烦,还有重复劳动。所以干脆单独提供一个对锁操作的类,由WriteLock与ReadLock持有使用,这个类叫Sync。
在这里插入图片描述

    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 6317671515068378041L;
        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; // 
获取低16位的条件
        // 获取读锁重入数
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        // 获取写锁重入数
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

我们都知道AQS中维护了一个state状态变量,正常来说,维护读锁与写锁状态需要两个变量,但是为了节约资源,使用高低位切割实现state状态变量维护两种状态,即高16位表示读状态,低16位表示写状态。

Sync中还定义了HoldCounter与ThreadLocalHoldCounter:
HoldCounter是用来记录读锁重入数的对象
ThreadLocalHoldCounter是ThreadLocal变量

        static final class HoldCounter {
            // 读锁重入数
            int count = 0;
            // 线程ID
            final long tid = getThreadId(Thread.currentThread());
        }

        static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                // 线程变量初始化
                return new HoldCounter();
            }
        }

4 公平与非公平策略

在AQS流程中,获取锁失败的线程,会被构建成节点入队到CLH队列,其他线程释放锁会唤醒CLH队列的线程重新竞争锁,如下图所示(简化流程):
在这里插入图片描述
非公平策略是指:非CLH队列的线程与CLH队列的线程竞争锁,大家各凭本事,不会因为你是CLH队列的线程,排了很久的队,就把锁让给你。
公平策略是指:严格按照CLH队列顺序获取锁,一定会让CLH队列线程竞争成功,如果非CLH队列线程一直占用时间片,那就一直失败,直到时间片轮到CLH队列线程为止,所以公平策略的性能会更差。
在这里插入图片描述
为了支持公平与非公平策略,Sync扩展了FairSync、NonfairSync子类,两个子类实现了readerShouldBlock、writerShouldBlock函数,即读锁与写锁是否阻塞。
在这里插入图片描述

5 ReentrantReadWriteLock全局图

在这里插入图片描述

6 深入细节逐个击破

6.1 ReentrantReadWriteLock的创建

   // 使用默认(非公平)属性创建一个新的 ReentrantReadWriteLock。
    public ReentrantReadWriteLock() {
        this(false);
    }
    // 使用给定的公平策略创建一个新的 ReentrantReadWriteLock。
    // fair – 如果此锁应使用公平排序策略,则为 true
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        // 创建读锁
        readerLock = new ReadLock(this);
        // 创建写锁
        writerLock = new WriteLock(this);
    }

ReentrantReadWriteLock默认是非公平策略,如果想用公平策略,可以直接调用有参构造器,传入true即可。但不管是创建FairSync还是NonfairSync,都会触发Sync的无参构造器,因为Sync是它们的父类(本质上它们俩都是Sync)。

        Sync() {
            // 当前线程持有的可重入读锁数。 仅在构造函数和 readObject 中初始化。 每当线程的读取保持计数降至 0 时删除。
            readHolds = new ThreadLocalHoldCounter();
            // 设置资源状态值,当前是0
            setState(getState()); // ensures visibility of readHolds
        }

因为Sync需要提供给ReadLock与WriteLock使用,所以创建ReadLock与WriteLock时,会接收ReentrantReadWriteLock对象作为入参。

        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
        protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }

最后通过ReentrantReadWriteLock.sync把Sync交给了ReadLock与WriteLock。

6.2 获取写锁

我们遵守ReadWriteLock接口规范,调用ReentrantReadWriteLock.writeLock函数获取写锁对象。

public class ReentrantReadWriteLockimplements ReadWriteLock, java.io.Serializable {
    private static final long serialVersionUID = -6992448646407690164L;
    private final ReentrantReadWriteLock.ReadLock readerLock;
    private final ReentrantReadWriteLock.WriteLock writerLock;
    final Sync sync;
    public ReentrantReadWriteLock() {
        this(false);
    }
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
    // 获取写锁对象
    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    // 获取读锁对象
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }
    // ...

获取到写锁对象后,遵守Lock接口规范,调用lock函数获取写锁。WriteLock.lock函数是由Sync实现的(FairSync或NonfairSync)。

    public static class WriteLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -4992448646407690164L;
        private final Sync sync;
        protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
        public void lock() {
            sync.acquire(1);
        }
        // ...

sync.acquire(1)函数是AQS中的独占式获取锁流程模板(Sync继承自AQS):

    // 独占式获取锁流程模板
    public final void acquire(int arg) {
        // 获独占式锁,Sync重写tryAcquire函数  
        if (!tryAcquire(arg) && // 获取独占锁失败
            // 自旋阻塞等待获取资源,并返回线程是否被中断过标记
            acquireQueued(addWaiter(Node.EXCLUSIVE)// 创建独占式标记节点 , arg))
            selfInterrupt();  // 如果线程被中断过,指向线程中断操作
    }

在这里插入图片描述

        protected final boolean tryAcquire(int acquires) {
            // 获取当前线程
            Thread current = Thread.currentThread();
            // 获取state状态
            int c = getState();
            // 计算写锁数量
            int w = exclusiveCount(c);
            if (c != 0) {
                // c != 0 && w ==0 表示存在读锁
                // or
                // 当前线程不是已经获取读锁的线程
                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;
            }
            // 如果没有锁,写锁是否阻塞。 writerShouldBlock-公平与非公平实现不一样
            //写锁是否阻塞 or cas同步状态失败返回false
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            // 设置当前线程
            setExclusiveOwnerThread(current);
            return true;
        }

流程图如下所示:
在这里插入图片描述
通过流程图,我们发现了一些要点:

读写互斥
写写互斥
写锁支持同一个线程重入
writerShouldBlock写锁是否阻塞实现取决公平与非公平的策略(FairSyncNonfairSync

6.3 释放写锁

获取到写锁,临界区执行完,要记得释放写锁(如果重入多次要释放对应的次数),不然会阻塞其他线程的读写操作,调用unlock函数释放写锁(Lock接口规范)。WriteLock.unlock函数也是由Sync实现的(FairSync或NonfairSync)。
在这里插入图片描述
sync.release(1)执行的是AQS中的独占式释放锁流程模板(Sync继承自AQS)。
在这里插入图片描述
在这里插入图片描述
tryRelease函数代码如下:
在这里插入图片描述
在这里插入图片描述
因为同一个线程可以对相同的写锁重入多次,所以也要释放的相同的次数。

6.4 获取读锁

我们遵守ReadWriteLock接口规范,调用ReentrantReadWriteLock.readLock函数获取读锁对象。
在这里插入图片描述
获取到读锁对象后,遵守Lock接口规范,调用lock函数获取读锁。ReadLock.lock函数是由Sync实现的(FairSync或NonfairSync)。
在这里插入图片描述
sync.acquireShared(1)函数执行的是AQS中的共享式获取锁流程模板(Sync继承自AQS)。
在这里插入图片描述

在这里插入图片描述
我们只关注tryAcquireShared函数,doAcquireShared函数是AQS的获取共享式锁失败后的流程内容,不属于本文范畴,tryAcquireShared函数代码如下:
在这里插入图片描述

在这里插入图片描述
通过流程图,我们发现了一些要点:

读锁共享,读读不互斥
读锁可重入,每个获取读锁的线程都会记录对应的重入数
读写互斥,锁降级场景除外
支持锁降级,持有写锁的线程,可以获取读锁,但是后续要记得把读锁和写锁读释放
readerShouldBlock读锁是否阻塞实现取决公平与非公平的策略(FairSyncNonfairSync

6.5 释放读锁

获取到读锁,执行完临界区后,要记得释放读锁(如果重入多次要释放对应的次数),不然会阻塞其他线程的写操作,通过调用unlock函数释放读锁(Lock接口规范)。ReadLock.unlock函数也是由Sync实现的(FairSync或NonfairSync)。
在这里插入图片描述
sync.releaseShared(1)函数执行的是AQS中的共享式释放锁流程模板(Sync继承自AQS)。

在这里插入图片描述
在这里插入图片描述
我们只关注tryReleaseShared函数,doReleaseShared函数是AQS的释放共享式锁成功后的流程内容,不属于本文范畴,tryReleaseShared函数代码如下:
在这里插入图片描述
在这里插入图片描述
这里有三点需要注意:

第一点:线程读锁的重入数与读锁数量是两个概念,线程读锁的重入数是每个线程获取同一个读锁的次数,读锁数量则是所有线程的读锁重入数总和。
第二点:AQS的共享式释放锁流程模板中,只有全部的读锁被释放了,才会去执行doReleaseShared函数
第三点:因为使用的是AQS共享式流程模板,如果CLH队列后面的线程节点都是因写锁阻塞的读锁线程节点,会传播唤醒

ReentrantReadWriteLock底层实现与ReentrantLock思路一致,它们都离不开AQS,都是声明一个继承AQS的Sync,并在Sync下扩展公平与非公平策略,后续的锁相关操作都委托给公平与非公平策略执行。

我们还发现,在AQS中除了独占式模板,还有共享式模板,它们在多线程访问共享资源的流程会有所差异,就如ReentrantReadWriteLock中读锁使用共享式,写锁使用独占式。

读读不阻塞
写锁阻塞写之后的读写锁,但是不阻塞写锁之前的读锁线程
写锁会被写之前的读写锁阻塞
读锁节点唤醒会无条件传播唤醒CLH队列后面的读锁节点
写锁可以降级为读锁,防止更新丢失
读锁、写锁都支持重入
举报

相关推荐

0 条评论