• AQS之ReentrantReadWriteLock分析 (九)


    1.ReentrantReadWriteLock 介绍

    在这里插入图片描述

    ReentrantReadWriteLock即可重入读写锁,其同时应用了共享锁和排斥锁,写锁使用排斥锁,读锁使用共享锁,从而实现读读共享,读写互斥,写写互斥。

    当读操作远远高于写操作时,这时候使用读写锁让读——读可以并发,提高性能。

    1.1 Sync

    读写锁使用的是一个Sync同步器(使用一个对象),可以分别创建。有公平锁和非公平锁两种子类进行实现。

    1.2 state

    一个32位的二进制数,分成两部分,高16位用于共享锁,表示持有锁的线程数;低16位用于排斥锁,表示锁重入次数。

    • 读状态的获得(获得高16位状态):state >>> 16(无符号补0右移16位)
    • 写状态的获得(获得低16位状态):state & 0x0000ffff(将高16位全部抹去)

    在这里插入图片描述

    1.3 ReadLock和WriteLock

    ReadLockWriteLockReentrantReadWriteLock的内部类,都实现了Lock接口,分别表示读锁和写锁,用老控制读和写操作,
    使用中,需要通过调用public ReentrantReadWriteLock.ReadLock readLock()public ReentrantReadWriteLock.WriteLock writeLock()获取到对应的读锁和写锁,然后进行lockunlock进行锁的获取和释放

    ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    // 读锁
    ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    // 写锁
    ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.读写锁

    当state等于0 --> 写状态和读状态都为0,表示未获得锁
    当state不等于0,当写状态等于0时 --> 读状态大于0,表示获得读锁
    当state不等于0,当读状态等于0时 --> 写状态大于0,表示获得写锁
    当state不等于0,当读状态和写状态都不等于0时 --> 发生锁的降级

    2.1 锁的降级

    锁的降级: 获取写锁 -> 获取写锁 -> 释放写锁 -> 释放读锁

    在这里插入图片描述

        public static void main(String[] args) {
            ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
            // 读锁
            ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
            // 写锁
            ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
    
            writeLock.lock();
            System.out.println("1");
            readLock.lock();
            System.out.println("2");
            writeLock.unlock();
            System.out.println("3");
            readLock.unlock();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    在这里插入图片描述

    这里如何使得读状态=2, 写状态=3的, 一个线程获取到写锁, 然后重入2次, 然后发生锁降级, 在释放写锁的时候, 获取到读锁, 并且重入一次。

    3.读写锁源码分析

    读写锁用的是同一个Sync同步器,所以有相同的阻塞队列和state

    在这里插入图片描述

    在这里插入图片描述

    • FairSync: Sync的公平锁的实现
    • NonfairSync: Sync的非公平锁的实现
    • ReadLock和WriteLock: 其实和ReentrantLock相同的结构, 只不过实现的方法不同

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    3.1 Sync 分析

    在这里插入图片描述

    这里通过分析, 得出一个32位的二进制数,分成两部分,高16位用于共享锁,表示持有锁的线程数;低16位用于排斥锁,表示锁重入次数。

    3.2 加锁和解锁分析

    ReentrantReadWriteLock是一个读写双用锁,但是除非是同一个线程的重入,ReentrantReadWriteLock不可能同时有读锁和写锁的状态(某一时刻只能是读锁或写锁)。

    成为读锁时,只增加state计数,而exclucsiveOwner处依然指向空。

    成为写锁时,exclucsiveOwner指向该线程。

    3.2.1 加锁分析

    t1为写锁, t2为读锁

    在这里插入图片描述

    此时线程t1占有锁流程与之前讲的ReentrantLock相似,不同之处时写锁状态占了state的低16位,而读锁占了state的高16位。
    同ReentrantLock类似,t1同时也修改了state位的值,表示该锁已被占有。

    此时出现t2竞争,执行 r.lock() ,这时进入读锁的 sync . acquireShared (1)流程
    首先会进入 tryAcquireShared 流程。如果有写锁占据,那么 tryAcquireShared

    在这里插入图片描述

    在这里插入图片描述

    返回-1表示失败, 0表示成功, 但是后继节点不会唤醒, 正数也成功, 而且后面可以唤醒节点, 读写锁返回1, 此处由于t1占据锁,所以此处t2的尝试是失败的,于是返回-1。

        private void doAcquireShared(int arg) {
            final Node node = addWaiter(Node.SHARED);
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (;;) {
                    final Node p = node.predecessor();
                    if (p == head) {
                        int r = tryAcquireShared(arg);
                        if (r >= 0) {
                            setHeadAndPropagate(node, r);
                            p.next = null; // help GC
                            if (interrupted)
                                selfInterrupt();
                            failed = false;
                            return;
                        }
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        interrupted = true;
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    先调用 addWaiter 添加节点,不同之处在于节点被设置为 Node . SHARED 模式(此处我们假设t2要获取的是读锁)而非 Node . EXCLUSIVE(如果尝试获取的是写锁) 模式,注意此时t2仍处于活跃状态。

    调用 tryAcquireShared (1)来尝试获取锁, 没成功的话, for (;;)循环一次, 把前驱节点的waitStatus改为-1。

    在这里插入图片描述

    此时如果依然有线程来争夺锁资源,则重复t2竞争时的策略,加入到队列中

    3.2.2 解锁分析

    写锁解锁, 调用unlock() -> sync.release(1)

    在这里插入图片描述

    先调用tryRelease(), 成功接下来执行唤醒流程unparkSuccessor, 让t1恢复运行

    读锁解锁, unlock() -> releaseShared(1)

    在这里插入图片描述

    3.3 加锁解锁总结

    当一个线程释放锁时,回在队列中的线程就会开始抢占锁资源,如果排在第二位(第一位是占位节点)的节点是Shared,由于读锁是共享锁,所以第二位的线程获得读锁以后,会继续检查其他线程的状态,如果还是Shared,则继续使线程获得锁,直到遇到Ex线程为止。但此时的exclusiveOwner处还是空的(因为此时的Sync是读锁)。

    如果第二个节点为Ex(即需要获得的是写锁,则仅仅使该线程获得锁,exclusiveOwner指向该线程。

    • 成为读锁时,只增加state计数,而exclucsiveOwner处依然指向空。

    • 成为写锁时,exclucsiveOwner指向该线程。

  • 相关阅读:
    【剑指 Offer 10- I. 斐波那契数列】
    【北京迅为】《i.MX8MM嵌入式Linux开发指南》-第一篇 嵌入式Linux入门篇-第六章 Vim 编辑器的使用
    电池的健康状态 SOH 估计
    笙默考试管理系统-MyExamTest----codemirror(14)
    wps 开发插件
    MySQL操作1——库的的操作(增删查改)
    阿里巴巴资深架构师熬几个通宵肛出来的Spark+Hadoop+中台实战pdf
    广告牌安全监测系统,用科技护航大型广告牌安全
    vim 从嫌弃到依赖(20)——global 命令
    Win11小组件提示加载此内容时出现错误怎么解决?
  • 原文地址:https://blog.csdn.net/qq_43141726/article/details/127825756