ReetrantReadWriteLock 读写锁原理

news2024/11/15 13:46:39

一、为什么要出现读写锁?

       我们知道synchronizer 和 ReentrantLock  都是互斥锁

       但现实很多业务场景都是读多写少,针对这种场景在并发中若采用  synchronizer 和

       ReentrantLock  来保证原子性,但会降低代码的性能。这种场景,就可以使用读写锁

       ReetrantReadWriteLock 来保证原子性,对于读操作使用读锁,对于写操作使用写锁,

       读锁与读锁之间不是互斥的,读操作之间可以并发执行,但写锁与读锁之间、写锁与

       写锁之间是互斥的,即只要涉及到了写操作也是需要互斥的。

       ReetrantReadWriteLock 读写锁使用示例:

           

public class ReentrantReadWriteLockDemo {

    static volatile int a=0;
    public static void readA(){
        System.out.println("a = "+a);
    }
    public static void writeA(){
        a++;
    }

    public static void main(String[] args) {

        /**
         * 读写锁适用于读多写少的场景,读读并发,读写、写读、写写互斥,读读兼容
         */
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock  = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

        Thread read1 = new Thread(()->{
            readLock.lock();
            try{
                readA();
            }finally {
                readLock.unlock();
            }
        });
        Thread read2 = new Thread(()->{
            readLock.lock();
            try{
                readA();
            }finally {
                readLock.unlock();
            }
        });

        Thread write1 = new Thread(() -> {
            writeLock.lock();
            try{
                writeA();
            }finally {
                writeLock.unlock();
            }
        });

        read1.start();
        read2.start();
        write1.start();

    }
}

       

二、读写锁的实现原理

       ReetrantReadWriteLock 也是基于AQS实现的,也是对AQS中的节点状态state进行操作,若

       线程获取到了锁资源就去执行后边的业务,若没有获取锁资源就把当前线程包装成node,然

       后将node放入AQS的阻塞队列中排队。

       但有一点要注意,即:

              读锁操作:基于state的高16位操作

              写锁操作:基于state的低16位操作

2.1、ReetrantReadWriteLock锁重入的问题:

              ReetrantReadWriteLock也是可重入锁

              1)写锁重入:读写锁种的写锁重入方式与ReetrantLock锁的重入方式一样,都是对AQS

                                      的state加1操作,只要确认持有锁的线程是当前写锁线程即可;区别是前

                                       面ReetrantLock的重入次数是state的整数,而写锁的重入次数是state的低

                                       16位,重入次数范围变小了。

              2)读锁重入:因为读锁是共享锁,读锁在获取锁资源操作时,需要对AQS的state的高16

                                      位进行+1操作;因为读锁允许统一时刻多个线程同时持有读锁,这样当多

                                      个读操作在持有读锁时,无法确认每个线程持有读锁的重入次数;为了记

                                       录读锁的重入次数,每个线程都有一个ThreadLocal 来记录其读锁的重入

                                       次数。

              3)写锁的饥饿问题:读锁是共享锁,当有现成持有读锁资源时,再来一个线程想获取读

                                       锁,直接对state修改即可;在读锁资源先被占用后,来了一个写锁资源,

                                       此时,当大量需要获取读锁的线程来请求锁资源时,如果可以绕过写锁直

                                       接获取读锁资源,会造成写锁长时间无法拿到写锁资源。

                                       读锁拿到锁资源后,如果再有读锁线程来获取锁资源,则需要去AQS阻塞

                                       队列排队,如果队列的前边有需要获取写锁的线程节点,那么后续读锁线

                                       程是无法拿到锁资源的,持有读锁的线程只会让写锁线程前边的读锁线程

                                        拿到锁资源。

2.2、ReetrantReadWriteLock内部类Sync核心属性解析

        ReetrantReadWriteLock.Sync 核心属性如下:

abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 6317671515068378041L;

        /*
         *
         * 读vs写计数提取常量和函数。
         * 锁状态逻辑上分为两个unsigned short:
         * 下一个代表exclusive (writer)锁持有计数,上一个代表shared (reader)锁持有计数。
         *
         * 读锁和写锁都有锁重入
         *
         * todo 注意:
         *     AQS 的状态state是32位(int 类型)的,辦成两份,
         *     读锁用高16位,表示持有读锁的线程数(sharedCount),
         *     写锁低16位,表示写锁的重入次数 (exclusiveCount)。
         *     状态值为 0 表示锁空闲,
         *     sharedCount不为 0 表示分配了读锁,
         *     exclusiveCount 不为 0 表示分配了写锁,
         *     sharedCount和exclusiveCount 一般不会同时不为 0,只有当线程占用了写锁,该线程可以重入获取读锁,反之不成立
         */
        /**
         * 读锁可以多个线程持有,而写锁只能有一个线程持有,所以称读锁--共享锁,写锁--互斥锁(排它锁),
         * 我们在AQS中了解到,使用32位的全局变量state来保存锁的重入次数,这里我们将state分割
         * 为高16位和低16位,其中高16位用来表示读锁
         */

        //位数
        static final int SHARED_SHIFT   = 16;
        //由于读锁使用高位,所以最大值加1,其实是AQS状态值加2^26
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);//共享锁,2^16 ,对读锁进行操作
        //写锁的可重入最大次数、读锁的最大数量
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;//2^16 -1
        //写锁的掩码,用于state的低16的值
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;//2^16 -1

        /*
         * 读锁计数器,当前持有读锁的线程数
         * 返回同步状态的高位16位的数值
         * 读锁的个数或重入数
         * >>>:二进制运算符,表示右移
         */
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        /*
         * 写锁计数器,写锁的重入次数,即写锁的线程数量
         /
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

        /**
         * 内部类
         * 一个用于记录每个线程持有读锁的计数器。存放在ThreadLocal中;
         * 每个线程应用读写锁的线程都有一个HoldCounter对象
         *
         * todo 问题 :若想知道每个线程对于读锁的重入次数,该如何做?
         *            使用ThreadLocal来进行统计,每个线程统计自己的
         */
        /**
         * A counter for per-thread read hold counts.
         * Maintained as a ThreadLocal; cached in cachedHoldCounter
         */
        static final class HoldCounter {
            int count = 0;
            //这个值是常量,使用id而不是线程的引用,是为了避免产生大量的垃圾
            //  所使用线程的ID,初始值是当前线程的ID
            final long tid = getThreadId(Thread.currentThread());
        }

        /**
         * 内部类
         * ThreadLocal 的子类,用来缓存每个线程的 HoldCounter;
         * 采用继承是为了重写 initialValue() 方法,若 ThreadLocal 中没有
         * 当前线程的计数,则 new 一个,这样就可以直接调用 ThreadLocal.get()
         */
        static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

        /*
         * 保存当前线程重入读锁次数的容器,当次数为0时移除
         * 创建ThreadLocal对象
         */
        private transient ThreadLocalHoldCounter readHolds;

        /**
         *
         *
         * 最近一个成功获取读锁的线程的计数器,这节省了ThreadLocal查找,通常情况下下一个要释放的线程是最后一个要获取线程,
         * 这不是volatile 的,因为这只是一个试探的动作,线程缓存也是可以的(因为判断是否是当前线程是通过线程id来判断的)
         */
        /**
         * 缓存最后一个成功获取读锁的线程的重入次数,有两方面的好处:
         * 1、避免了通过访问ThreadLocal来获取读锁的信息,这个优化的前提是
         *    假设多数情况下,一个获取读锁的线程,使用完以后就会释放读锁,
         *    也就是说最后获取读锁的线程和最先释放读锁的线程大多数情况下是同一个线程
         * 2、因为ThreadLocal中的key是一个弱引用类型,当有一个变量持有HoldCounter对象时,
         *    ThreadLocalHolderCounter中最后一个获取锁的线程信息不会被GC回收掉
         */
       
        private transient HoldCounter cachedHoldCounter;//保存最后一个线程的读锁数量

        /**
         * firstReader是第一个获得读锁的线程。
         * firstReaderHoldCount是firstReader的保持计数。
         * 更准确地说,firstReader是最后一次将共享计数从0更改为1的惟一线程,此后一直没有释放读锁;
         * 如果没有这样的线程,则为空。
         * 不能导致垃圾保留,除非线程终止而不放弃其读锁,因为tryreleasshared将其设置为null。
         * 通过良性数据竞争访问;依赖于内存模型对引用的out- thin-air保证。
         * 这使得跟踪非争用读锁的读持有非常便宜。
         *
         * firstReader 是一个特殊线程,他是最后一个 将共享计数从0更改为1的线程(在锁空闲的时候)
         * firstReaderHoldCount 是 firstReader 的可重入计数
         * firstReader 不能保留垃圾,所以在 tryreleasshared 里将其设置为null,除非线程是移除终止,没有
         * 释放读锁
         * firstReader 的作用是 跟踪无竞争的读锁计数器时非常便宜
         */
        /**
         * 第一个获取读锁的线程,有两方面的考虑:
         * 1、记录将共享数量从0变成1的线程
         * 2、对于无竞争的读锁来说进行线程重入次数数据的追踪的成本是比较低的
         */
        private transient Thread firstReader = null;//保存获取到该锁的第一个读锁线程
        //第一个获取读锁线程的重入次数
        private transient int firstReaderHoldCount;//保存该锁第一个线程获取的读锁数量

        Sync() {
            readHolds = new ThreadLocalHoldCounter();
            //若一个Java文件中同时包含volatile和非volatile 的属性,修改 volatile 修饰的属性时,非volatile
            //修饰的属性也会立即对其他线程可见,等价于volatile 修饰,即保证可见性
            setState(getState()); // ensures visibility of readHolds 确保readholds的可见性
        }
}

三、写锁分析

3.1、写锁加锁流程概述

           

3.2、写锁加锁流程分析

3.2.1、lock() 方法(写锁的lock() 方法)

            该方法是写锁加锁的入口,在该方法中调用Sync 的 acquire() 的方法来实现加锁的;

            而Sync 是AQS的子类(acquire() 就是AQS中的方法,请参考前边AQS笔记)。

            在AQS的 acquire() 方法中我们重点关注的是方法 tryAcquire在各个子类中的实现,

            这里我们需要关注 tryAcquire在 ReetrantReadWriteLock.Sync 类中实现,acquire()

            方法中调用的其他方法请参考AQS的笔记。

            lock() 方法代码:

                  

3.2.2、tryAcquire(int arg) 方法

            该方法是 ReetrantReadWriteLock.Sync 中实现AQS中的方法,其功能是获取写锁

            tryAcquire 中的方法 writerShouldBlock区分了写锁的公平锁与非公平锁的不同之处。

            tryAcquire 方法代码如下:

             

/**
         * 获取写锁:该方法由AQS调用,用于判定其子类的上锁逻辑;逻辑和原有获取互斥锁保持一致
         */
        protected final boolean tryAcquire(int acquires) {
            /*
             * 有下面条件之一则获取锁失败:
             * 1、如果读计数器不为0或写计数器不为0 ,或持有锁的线程不是同一个线程(即不是当前线程)则获取锁失败
             * 2、如果计数器达到上限,则获取锁失败(这只能发生在计数器已经非0的情况)
             * 3、其他的,这个线程有资格获取锁,如果该锁是可重入的或符合队列策略,获得锁后需要更新持有锁的
             *    同步状态
             */
            //获取当前线程
            Thread current = Thread.currentThread();
            //获取当前同步状态,即锁的状态 0=表示没有线程持有该锁
            //获取当前状态值和互斥锁的数量
            int c = getState();
            //返回写锁的数量,互斥锁一般是写锁
            int w = exclusiveCount(c);//获取state低16位的值
            if (c != 0) {//表示有线程持有该锁
                //锁资源已经被读锁独占 或 正在持有写锁的线程不是当前线程,则获取锁失败
                // (Note: if c != 0 and w == 0 then shared count != 0)
                if (w == 0 || //w =0 表示没有线程持有写锁,
                        current != getExclusiveOwnerThread())
                    //获取锁失败,让AQS对当前线程执行阻塞操作,即将当前线程放入阻塞队列
                    return false;
                //代码走到这里表示当前线程已经持有写锁
                //写锁重入次数超过上限 MAX_COUNT,则抛出异常
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire 获取到锁,并修改同步状态
                setState(c + acquires);
                return true;
            }
            //执行到这里既没有现成持有读锁也没有线程持有写锁,则尝试获取锁资源
            //锁没有被其他线程持有的情况下,判断写锁是否需要阻塞或是否符合队列策略,
            //若不需要阻塞,则修改同步状态(即竞争锁),操作失败则返回失败,获取锁失败
            if (writerShouldBlock() ||  //由子类实现判断当前线程是否应该获取写锁
                !compareAndSetState(c, c + acquires))  //通过CAS抢写锁
                //抢写锁失败
                return false;
            //若当前线程获取了锁,则设置持有锁的线程
            setExclusiveOwnerThread(current);//抢写锁成功,则将当前线程标识为获取互斥锁的线程对象
            return true;
        }



/**
         * 公平锁下,writerShouldBlock和readerShouldBlock方法都表示当有别的线程也在尝试获取锁时,是否应该阻塞。
         * 对于公平模式,hasQueuedPredecessors()方法表示前面是否有等待线程。一旦前面有等待线程,那么为了遵循公平,
         * 当前线程也就应该被挂起。
         *
         *  这里区分公平锁 与 非公共平锁的不同之处
         * 公平锁 ReentrantReadWriteLock.FairSync 类中的方法
         * @return
         */
        final boolean writerShouldBlock() {
            /**
             * 判断AQS中的阻塞队列中是否有其他线程正在等待唤醒,若有,则当前线程需要进入阻塞队列中排队
             */
            return hasQueuedPredecessors();
        }


/**
         * 非公平模式下,writerShouldBlock直接返回false,说明不需要阻塞
         *
         * 非公平锁 ReentrantReadWriteLock.NonFairSync 类中的方法
         * @return
         */
        final boolean writerShouldBlock() {
            //读写锁的本身就是为了解决读多写少的场景,此时不应该让写锁饥饿
            //而且非公平锁的写锁永远不阻塞,可以直接去竞争锁
            return false; // writers can always barge
        }

3.3、写锁释放锁流程分析

         写锁释放锁的入口是unlock() 方法,在unlock() 方法中调用ReetrantReadWriteLock.Sync的

         release()方法,即AQS的release方法,如下图所示:

                 

                

         在release()方法中我们重点关注 tryRelease()方法,tryRelease方法由AQS定义,但在AQS

          的各个子类中实现,这里我们需要关注在AQS子类 ReetrantReadWriteLock.Sync 中的实现

           tryRelease() 在 ReetrantReadWriteLock.Sync 代码如下

 //独占式下释放锁,即释放写锁
        protected final boolean tryRelease(int releases) {
            /**
             * 判断是否获取到了写锁,若没有写锁则抛出异常
             * 没有获取写锁,为啥需要释放写锁呢?
             */
            //判断持有写锁的线程是否是当前线程
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            //获取state
            int nextc = getState() - releases;
            //线程持有锁的个数是否为0,为0表示没有线程持有锁,即锁已经释放了
            //释放完毕后,写锁状态为0
            /**
             * 写锁释放为0,判断写锁计数器是否为0,为0表示写锁释放成功
             * 为什么要判断是否为0?
             * 1、锁的重入,不为0表示有锁的重入
             * 2、当前线程释放写锁,由于非公平锁的原因,B线程立即持有写锁,这时写锁的个数又大于0了
             * 这里考虑的是锁重入
             */
            boolean free = exclusiveCount(nextc) == 0;
            //若nextc 等于0,表明当前线程完全释放了锁,也即锁重入为0,将当前线程从ownerThread对象中移除
            if (free)
                setExclusiveOwnerThread(null);
            //设置同步状态,即设置全局的state变量的值
            setState(nextc);
            //返回锁是否释放成功,若返回True,则表示释放锁成功,由AQS完成后面阻塞队列中的线程的唤醒
            return free;
        }

四、读锁分析

4.1、读锁加锁流程概述

         

4.2、读锁加锁流程分析

4.2.1、lock()(读锁 ReadLock 的lock方法)

            该方法是读锁加锁的入口,在该方法中调用ReetrantReadWriteLock.Sync 的

            acquireShared(),Sync是AQS的子类,acquireShared 方法其实是AQS的方法。

            如下图所示:

                    

                     

            在 acquireShared 方法中这里我们只需要方法 tryAcquireShared,其他方法请参照前边笔

            记AQS(二)。

             方法 tryAcquireShared在AQS中定义,但在AQS的各个子类中实现,这里我们需要关注

             子类 ReetrantReadWriteLock.Sync 中的实现   

           

4.2.2、tryAcquireShared(int unused) 方法

           该方法功能是获取读锁

           tryAcquireShared 中方法 readerShouldBlock() 区分了读锁的公平锁与非公平锁的不同

           的地方,

          readerShouldBlock() 方法功能是判断读锁线程是否应该被阻塞,其在类 

          ReetrantReadWriteLock 定义,分别在 内部类 ReetrantReadWriteLock.FairSync 和

          ReetrantReadWriteLock.NonFairSync  中实现。

           tryAcquireShared方法代码如下:

              

protected final int tryAcquireShared(int unused) {
            /*
             * 
             * 下面条件之一,则获取读锁失败:
             *  1、如果写锁被另外一个线程持有,则获取失败
             *  2、另外,如果这个线程可以获取锁则进入WRT状态,并根据阻塞队列(即公平锁)策略判断该线程是否需要阻塞。
             *     如果不需要阻塞,则通过CAS来更新同步状态(即获取锁)、更新读/写锁计数器,则获取读锁成功。
             *     注意这一步没有检查 “可重入获得”(即检查锁是否可重入),可重入检查被推迟到完整版本,
             *     以避免在更典型的不可重入情况下必须检查hold count 读/写 锁计数器。
             *  3、如果步骤2失败,则进入fullTryAcquireShared 方法中尝试获取读锁
             *
             *  1)有线程持有写锁,且该线程不是当前线程,获取读锁失败。
             *  2)写锁空闲 且  公平锁策略决定 读线程应当被阻塞,除了重入获取,其他获取锁失败。
             *  3)读锁数量达到最多,抛出异常。
             * 除了以上三种情况,该线程会循环尝试获取读锁直到成功。
             */
            //获取当前线程
            Thread current = Thread.currentThread();
            //获取锁的同步状态值
            int c = getState();
            // 锁资源已经被写锁独占,且 当前持有锁的线程不是当前线程,则返回-1,告诉AQS获取共享锁失败
            if (exclusiveCount(c) != 0 &&  //有没有线程持有写锁
                getExclusiveOwnerThread() != current) //有线程持有写锁,则继续判断持有写锁的线程是否是当前线程,若是则返回-1,告诉AQS获取共享锁失败
                return -1;
            //获取读锁计数,持有读锁的线程数
            int r = sharedCount(c);
            /**
             * 1、读锁不需要等待
             * 2、读锁未超过上限
             * 3、设置读锁的state值成功
             * 则返回成功
             */
            if (!readerShouldBlock() &&//让子类来判定获取读锁的线程是否应该被阻塞
                r < MAX_COUNT &&//读锁未超过上限,即释放发生了溢出
                compareAndSetState(c, c + SHARED_UNIT)) {//CAS增加state高16位读锁的持有数量,即获取共享锁成功

                //增加state高16位(读锁数量)之前的计数器为0,表明当前线程就是第一个获取读锁的线程
                if (r == 0) {
                    //使用2个线程来优化ThreadLocal
                    //记录第一个获取读锁的线程信
                    firstReader = current;//
                    //读锁第一次被获取,则读锁的重入数为 1
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    //持有读锁的线程数不为0(即读锁已经有线程在持有),这时若firstReader为当前线程,
                    // 则这时当前线程算重入读锁, firstReader 计数器firstReaderHoldCount加1,
                    // 即第一个获取读锁的线程再次获取锁(重入)
                    firstReaderHoldCount++;
                } else {
                    //如果读锁计数不等于0,firstReader也不是当前线程,此时将获取读取锁的次数保存在ThreadLocal中;
                    HoldCounter rh = cachedHoldCounter;
                    //如果 cachedHoldCounter 的值为null 或 最近一个获取读锁的线程不是当前线程
                    if (rh == null || rh.tid != getThreadId(current))
                        //以当前线程读锁的重入次数来更新 cachedHoldCounter
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)//表示最后一个线程持有的读锁数量为0,即此时没有线程持有读锁
                        //若 当前线程持有的读锁数量保存在ThreadLocal 中,即当前线程是最后一个持有读锁的线程
                        readHolds.set(rh);
                    //最后将读锁的重入次数加1
                    rh.count++;
                }
                return 1;
            }
            //todo 前面额判断是为了在前置优化 fullTryAcquireShared
            //获取读锁失败,进入 fullTryAcquireShared 中继续尝试获取
            return fullTryAcquireShared(current);
        }

4.2.3、readerShouldBlock() 方法

            该方法功能是判断读锁线程是否应该被阻塞;其在类 ReetrantReadWriteLock 定义,

            分别在 内部类 ReetrantReadWriteLock.FairSync 和ReetrantReadWriteLock.NonFairSync 

            中实现。

            readerShouldBlock 方法代码如下:

/**
    在 FairSync 中的实现
*/
final boolean readerShouldBlock() {
            return hasQueuedPredecessors();//看前面是否有读锁在排队
        }

//判断前面是否有读锁在排队
public final boolean hasQueuedPredecessors() {
       
        Node t = tail; 
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }


//===============================================================================


/**
         * 判断读锁是否需要阻塞
         *
         * 解决写锁线程饥饿:
         *   在获取读锁之前看下是否有写锁线程在阻塞排队
         *
         *  在 NonFairSync 中的实现
         */
       
        final boolean readerShouldBlock() {
            
            return apparentlyFirstQueuedIsExclusive();//看队列中第一个等待的线程是否是互斥锁,即写锁
        }
    }


//看队列中第一个等待的线程是否是互斥锁,即写锁

 final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
    }

4.2.4、fullTryAcquireShared(Thread current) 方法

            该方法功能是通过自旋的方式不停的尝试获取读锁

            fullTryAcquireShared 方法代码如下:

              

final int fullTryAcquireShared(Thread current) {
            /*
             * 
             * 调用该方法的线程都是希望获取读锁的线程,有3种情况:
             * 1、在尝试通过CAS操作修改state时由于有多个竞争读锁的线程导致CAS操作失败
             * 2、需要排队等待获取读锁的线程(公平锁)
             * 3、超过读锁限制的最大申请次数的线程
             */
            HoldCounter rh = null;//当前线程获取读锁的数量
            //自旋,获取读锁
            for (;;) {
                //获取锁状态
                int c = getState();
                //若资源已经被写锁独占了,且持有写锁的线程不是指定的线程 current,则直接返回获取锁失败
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current) //锁资源被其他写锁线程占用
                        return -1;
                    // else we hold the exclusive lock; blocking here 否则我们持有互斥锁;阻塞会导致死锁。
                    // would cause deadlock.
                } else if (readerShouldBlock()) {//子类判断当前读线程是否需要阻塞,若需要阻塞则进入判断
                    //  确保我们不是以可重入的方式获取读锁
                    if (firstReader == current) {//若 firstReader 是当前线程,则不做任何处理
                        // assert firstReaderHoldCount > 0;
                    } else {
                        //获取当前线程记录读锁重入次数的 HoldCount对象
                        //不是第一个读线程,则对ThreadLocal 操作
                        // 清理当前线程中重入次数为0的数据
                        if (rh == null) {//这个判断是为了获取当前线程读锁的重入次数
                            //最近一个获取读锁的计数器
                            rh = cachedHoldCounter;
                            //若最近获取读锁的线程不是当前线程current,且读锁的重入次数为0,则删除
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)//重入次数为0则删除
                                    readHolds.remove();
                            }
                        }
                        if (rh.count == 0)//若当前读锁的重入次数为0,则表示没有获取到阻塞,返回-1,阻塞当前线程
                            return -1;
                    }
                }
                if (sharedCount(c) == MAX_COUNT)//若读锁的个数到达了上限 MAX_COUNT,则抛出异常,获取锁失败
                    throw new Error("Maximum lock count exceeded");
                //CAS增加读锁次数,即获取读锁,若执行失败,则进入下一次循环
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    //表示当前线程是第一个获取到读锁的线程
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {//判断第一个线程是否是当前线程
                        //当前线程重复获取读锁(锁重入)
                        firstReaderHoldCount++;
                    } else {//操作ThreadLocal
                        // 在readHolds中记录获取锁的线程的信息
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; 
                    }
                    //获取锁成功
                    return 1;
                }
            }
        }

            

4.3、读锁释放锁流程分析

         读锁释放锁的入口是方法 unlock(),在 unlock 中调用ReetrantReadWriteLock.Sync的

         releaseShared方法去释放锁(其实是AQS的方法),如下图所示:

                 

                 

         在 releaseShared 方法中我们只需要关注 tryReleaseShared 方法,该方法在AQS中定义, 

         由其的各个子类实现,releaseShared 的其他方法请参考前边的AQS(二) 笔记

         在这里,我们需要看下 releaseShared 方法在 ReetrantReadWriteLock.Sync 子类中的实现

         ReetrantReadWriteLock.Sync 中的方法 releaseShared 的代码如下:

            

/**
         * 释放读锁,需要考虑:
         *    重入多少次,就要减多少次
         *    先完成自己线程的读锁的释放,然后再CAS完成state高16位读锁的释放
         */
        protected final boolean tryReleaseShared(int unused) {
            //获取当前线程
            Thread current = Thread.currentThread();
            /**
             * 如果当前线程是第一个获取读锁的线程,有两种情况:
             * 1、如果持有锁的次数为1,直接释放成功
             * 2、如果持有锁的次数大于1,说明有重入的情况,需要次数减1
             */
            if (firstReader == current) {//当前线程是第一个获取到读锁的线程
                //若 当前线程持有锁的个数firstReaderHoldCount等于1,表示可以直接释放读锁,
                //否则,表示读锁有重入,firstReaderHoldCount自减1
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;//表示读锁有重入
            } else {//第一次持有锁的线程不是当前线程
                //获取线程持有锁的计数器
                HoldCounter rh = cachedHoldCounter;
                //若最后持有读锁的计数器为null 或 最后一个持有读锁的线程不是当前线程,
                //则获取当前线程对应的 HoldCounter
                if (rh == null || rh.tid != getThreadId(current))
                    //则获取当前线程对应的 HoldCounter 计数器,即读锁的个数
                    rh = readHolds.get();
                //获取当前线程重入锁的次数
                int count = rh.count;
                //若当前线程只持有一次读锁(即没有重入),则删除 readHolds 中的数据
                //若当前线程没有持有锁,则抛出异常
                if (count <= 1) {//小于1,表示当前线程已经释放完了读锁,不需要在ThreadLocal中持有HolderCount对象
                    readHolds.remove();
                    if (count <= 0)//非法锁状态
                        throw unmatchedUnlockException();
                }
                //计数器减1
                --rh.count;
            }
            //减共享状态state,即CAS释放state高16位的读锁
            //自旋,释放锁,并判断当前线程释放锁后,锁是否是空闲的(即nextc==0)
            for (;;) {
                int c = getState();
                // 如果是最后一个释放读锁的线程nextc为0,否则不是
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))//释放锁
                    //释放完成后判断是否为0,若为0 则表示所有的读锁都释放了,此时是无锁状态;
                    // 那么此时需要干些啥?释放完成后需要由AQS来唤醒后面的阻塞的线程
                    return nextc == 0;
            }
        }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2038695.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据库技术核心:迭代逻辑思考

文章目录 数据库演进史第一阶段&#xff1a;早期文件系统第二阶段&#xff1a;关系数据库&#xff08;RDBMS&#xff09;第三阶段&#xff1a;面向对象数据库&#xff08;OODBMS&#xff09;第四阶段&#xff1a;分布式数据库和 NoSQL 数据库第五阶段&#xff1a;NewSQL 数据库…

广告资料库是什么?如何正确使用Facebook广告资料库?一文解决你的烦恼!

什么是广告资料库 广告营销领域&#xff0c;创意和策略的更新速度极快。为了跟上这种节奏&#xff0c;广告资料库应运而生&#xff0c;成为广告人和营销专家的重要工具。广告资料库是一个集中存储和管理广告素材、创意案例、市场数据和用户反馈的平台。它不仅帮助用户获得灵感…

Redis集群:概念和部署示例

目录 Redis 集群的优点 集群模式 主从模式 缺陷 哨兵模式 缺陷 集群模式&#xff08;Redis Cluster&#xff09; 数据分片原理 添加节点 删除节点 Redis集群的分片方式 故障转移机制 如果Master只有一个Slave 如果Master有多个Slave 如果两个Slave票数一样呢&a…

UE5中动画重定向问题,新角色滑步,双脚不动向前后左右移动

UE5系列文章目录 我们在UE商场或者一些其他渠道的感觉非常好的人物模型和动画&#xff0c;想把他们替换到我们的工程中&#xff0c;或者把小白人替换到&#xff0c;就经常遇到动画重定向 Unreal Engine 5&#xff08;UE5&#xff09;中的动画重定向是一种强大的功能&#xff…

高校体育场管理小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;状态管理&#xff0c;学生管理&#xff0c;体育场管理&#xff0c;用户订单管理&#xff0c;学生订单&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;…

HTML5+JavaScript绘制彩虹和云朵

HTML5JavaScript绘制彩虹和云朵 彩虹&#xff0c;简称虹&#xff0c;是气象中的一种光学现象&#xff0c;当太阳光照射到半空中的水滴&#xff0c;光线被折射及反射&#xff0c;在天空上形成拱形的七彩光谱&#xff0c;由外圈至内圈呈红、橙、黄、绿、蓝、靛、紫七种颜色。事实…

RabbitMq消息队列(缓存加速)

然后切换yum仓库&#xff1b; 这里采用阿里的仓库&#xff1b; rm -rf /etc/yum.repos.d/* curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo curl -o /etc/yum.repos.d/epel.repo https://mirrors.aliyun.com/repo/epel-7.repo …

Vitis AI 基本认知(CNN基本概念)

目录 1. 目的 2. 概念解释 2.1 计算并行度 2.2 超参数 2.3 反向传播算法 2.4 优化器&#xff08;Optimizer&#xff09; 2.5 评估指标&#xff08;Metrics&#xff09; 2.5.1 准确率、精确率、召回率 2.5.2 F1 Score 2.5.3 IoU 2.6 内存布局 2.6.1 输入 2.6.2 中间…

【eNSP模拟实验】链路聚合-手工负载模式和静态LACP模式

链路聚合介绍 简介 链路聚合&#xff08;英语&#xff1a;Link Aggregation&#xff09;是一个计算机网络术语&#xff0c;指将多个物理端口汇聚在一起&#xff0c;形成一个逻辑端口&#xff0c;以实现出/入流量吞吐量在各成员端口的负荷分担&#xff0c;交换机根据用户配置的…

再谈表的约束

文章目录 自增长唯一键外键 自增长 auto_increment&#xff1a;当对应的字段&#xff0c;不给值&#xff0c;会自动的被系统触发&#xff0c;系统会从当前字段中已经有的最大值1操作&#xff0c;得到一个新的不同的值。通常和主键搭配使用&#xff0c;作为逻辑主键。 自增长的…

分享一个拿来即用的柱状图绘制函数

分享一个自己写的柱状图绘制函数&#xff0c;可用来绘制横向的多柱状图、堆积柱状图&#xff0c;纵向的多柱状图、堆积柱状图。便于我们方便快捷的绘制相应的柱状图。该函数参数不多&#xff0c;只用于观察数据形式是足够的&#xff0c;若要绘制更加精美的柱状图&#xff0c;大…

Avnet ZUBoard 1CG开发板上手—深度学习新选择

Avnet ZUBoard 1CG 开发板上手—深度学习新选择 摘要 本文主要介绍了 Avnet ZUBoard 1CG 开发板的特性、架构、硬件单元等概念&#xff0c;并对如何使用以太网接口和串口连接开发板进行基本介绍&#xff0c;同时辅以两个应用例程演示其功能。 原文链接&#xff1a; FreakSt…

clamp靶机复现

靶机设置 设置靶机为NAT模式 靶机IP发现 nmap 192.168.112.0/24 靶机IP为192.168.112.143 目录扫描 dirsearch 192.168.112.143 访问浏览器 提示让我们扫描更多的目录 换个更大的字典&#xff0c;扫出来一个 /nt4stopc/ 目录 目录拼接 拼接 /nt4stopc/ 发现页面中有很多…

数据结构----队列

一、队列 1&#xff09;队列定义 队列(Queue)是只允许在一端进行插入操作&#xff0c;而在另一端进行删除操作的线性表。 允许插入的端是队尾&#xff0c;允许删除的端是队头。队列是一个先进先出(FIFO)的线性表&#xff0c;相应 的也有顺序存储和链式存储两种方式。 2&#…

macOS Sonoma 14.6.1 (23G93) Boot ISO 原版可引导镜像下载

macOS Sonoma 14.6.1 (23G93) Boot ISO 原版可引导镜像下载 2024 年 8 月 8 日凌晨&#xff0c;macOS Sonoma 14.6.1 发布&#xff0c;本更新包含了重要的错误修复&#xff0c;并解决了导致高级数据保护无法启用或停用的问题。同时带来了 macOS Ventura 13.6.9 安全更新。 本…

ant-design源码解析——Upload上传组件

前言 文件上传是我们开发中不可或缺的一部分&#xff0c;我们将在本文深入解析Ant Design Upload组件的实现。 相信看完以后对于React以及Ant Design的工作原理理解能更上一层楼。 Upload.tsx 入口函数upload.tsx直接引用了AjaxUpload组件&#xff0c;引用了一些能力&#…

UVa1660/LA3031 Cable TV Network

UVa1660/LA3031 Cable TV Network 题目链接题意分析AC 代码 题目链接 本题是2004年icpc欧洲区域赛东南欧赛区的题目 题意 给定一个n&#xff08;n≤50&#xff09;个点的无向图&#xff0c;求它的点连通度&#xff0c;即最少删除多少个点&#xff0c;使得图不连通。如下图所示…

Java日志体系框架总结:JUL、JCL、SLF4J、Log4j、Logback、Log4j2

概述 日志记录是应用程序运行中必不可少的一部分。具有良好格式和完备信息的日志&#xff0c;可以在程序出现问题时帮助开发人员迅速地定位错误的根源。日志所能提供的功能是多种多样的&#xff0c;包括记录程序运行时产生的错误信息、状态信息、调试信息和执行时间信息等。 …

Day 22~28 MySQL

MySQL 1、数据库 JavaEE&#xff1a;企业级开发 Web 前端 &#xff08;页面&#xff1a;展示&#xff0c;数据&#xff09; 后台&#xff08;连接点&#xff1a;连接数据库JDBC&#xff0c;连接前端&#xff08;控制&#xff0c;控制视图跳转&#xff0c;给前端传递数据&…

dedecms织梦 验证码不显示问题

dedecms验证码不显示呢?近期小编仔细研究了一下并根据网上的各个版本总结下面几种解决方法&#xff1a; 问题一&#xff1a;首先先确定php配置环境没问题&#xff0c;如果一个服务器有的网站显示验证码有的不显示&#xff0c;可以排除运行环境的问题;出现这种情况有可能是&…