Java并发(十二):读写锁——读锁

回顾

前面我们已经看了写锁是怎样进行获取和释放的,下面就来看看读锁的获取和释放

读锁

写锁是一个排他锁,而读锁却是一个共享锁,而且还支持可重入,并且能被多个线程同时获取

读锁的获取

在这里插入图片描述
在这里插入图片描述
可以看到,其底层的实现也是AQS,只不过是AQS的共享模式,并且执行的是AQS的acquireShared方法

源码如下

    public final void acquireShared(int arg) {
        //执行tryAcquireShared方法尝试进行获取共享锁
        if (tryAcquireShared(arg) < 0)
            //获取共享锁失败,进行CAS自旋(写锁被获取了)
            doAcquireShared(arg);
    }

可以看到获取共享锁的主要方法为tryAcquireShared

tryAcquireShared

源码如下

        protected final int tryAcquireShared(int unused) {
            /*
             * Walkthrough:
             * 1. If write lock held by another thread, fail.
             * 2. Otherwise, this thread is eligible for
             *    lock wrt state, so ask if it should block
             *    because of queue policy. If not, try
             *    to grant by CASing state and updating count.
             *    Note that step does not check for reentrant
             *    acquires, which is postponed to full version
             *    to avoid having to check hold count in
             *    the more typical non-reentrant case.
             * 3. If step 2 fails either because thread
             *    apparently not eligible or CAS fails or count
             *    saturated, chain to version with full retry loop.
             */
            //依然从注释上开始看起,注释同样提到了三点
            //1.如果其他线程拥有了写锁,获取共享锁失败
            //2.如果没有写锁,就代表可以获取共享锁,不过要现判断是否应该进行阻塞(由于队列策略)
            //	并且通过CAS的方法更新同步状态,更新成功就代表获取锁成功
            //3.如果获取共享锁失败,代表CAS失败或者共享锁数量满了
            
            //获取当前线程
            Thread current = Thread.currentThread();
            //获取当前同步状态量
            int c = getState();
            //获取低位,也就是获取写锁的同步状态量
            //判断写锁的同步状态量是否为0,即有没有线程去获取写锁
            //如果写锁被人获取了,再判断写锁的拥有者是不是当前线程
            //因为规定拥有写锁的线程可以去拥有读锁
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                //如果写锁被获取了,且写锁不是被自己获取
                //获取共享锁失败,返回-1
                return -1;
            //获取共享锁的同步状态量
            int r = sharedCount(c);
            //判断获取共享锁需要不需要进行阻塞
            //判断共享锁的同步状态量是不是达到了最大值
            //判断CAS改变共享锁的同步状态量是否成功
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                //如果共享锁的同步状态量为0,证明是第一个去获取共享锁
                //注意此时这里会有并发问题
                //假如第一个线程在CAS这里停止了,那么后面线程获取的同步状态量依然为0
                if (r == 0) {
                    //将第一个获取共享锁的线程设置为当前线程
                    //这里是有并发问题的
                    firstReader = current;
                    //记录第一个线程获取共享锁的次数
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    //如果第一个获取共享锁的线程,又去获取共享锁
                    //次数+1,代表重入了
                    firstReaderHoldCount++;
                } else {
                    //如果是其他线程进来获取共享锁
                    //定义一个HoldCounter缓存
                    HoldCounter rh = cachedHoldCounter;
                    //HoldCounter缓存存储在ThreadLocalMap中
                    if (rh == null || rh.tid != getThreadId(current))
                        //如果缓存为空,代表第一次其他线程进来
                        //如果缓存不为空,但此时的缓存不属于当前线程(线程ID不对应)
                        //从ThreadLocal的ThreadLocalMap中去取当前线程的缓存
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        //如果缓存中的count为0
                        //将缓存添加进容器中
                        readHolds.set(rh);
                    //通过holdCounter记录其他线程获取的共享锁次数
                    rh.count++;
                }
                //返回1,代表加锁成功
                return 1;
            }
            //如果CAS失败或其他原因导致失败
            //调用fullTryAcquireShared方法
            //其实里面底层是
            return fullTryAcquireShared(current);
        }

从代码上可以看到,共享锁其实还挺复杂

  • 判断写锁有没有被线程获取

    • 如果写锁被获取了,再判断写锁是不是被当前线程获取,如果是当前线程获取,仍然可以继续去获取读锁
    • 如果写锁被获取了,且并不是当前线程获取的,直接reutnr -1,代表获取读锁失败
  • 判断是否需要进行阻塞

  • 判断读锁的同步状态量是否达到最大值

  • 判断CAS更换读锁的同步状态量是否成功

  • 只有满足上面三个判断,才代表获取锁成功

  • 获取锁成功之后就要进行记录重入次数了,虽然我们更改了读锁的同步状态量,但读锁的同步状态量是一个整体的,而读锁是共享的,即每个线程都有,所以需要每个线程去记录读锁的重入次数

    • 对于第一个获取共享锁的线程,进行记录,并且记录其重入次数
    • 对于其他线程,是使用ThreadLocal来进行存储的,具体的底层容器为ThreadLocalMap
  • 如果获取锁失败,会调用fullTryAcquireShared方法,这个方法的本质上是CAS进行tryAcquiredShared。。。

前面的步骤都还好理解,重点在于读锁怎么去存储各个线程的重入次数的

记录各个线程的重入次数

对于被firstReader记录的线程还好说,通过firstReaderHoldCount进行记录即可

但对于其他线程,则是利用ThreadLocalMap来进行存储的

HoldCounter

每个线程对应的缓存其实就是一个HoldCounter

在这里插入图片描述
可以看到,这个类里面有两个属性

  • count:记录重入次数
  • tid:记录线程ID

从前面可以看到,当前缓存为空,或者该缓存并不属于当前线程(线程ID不对应),那么就会走readHolds的get方法

从判断条件可以看到,这个get方法应该会做两种事情

  • 缓存为空,证明没创建,需要进行初始化
  • 缓存不属于当前线程,需要进行切换线程

在这里插入图片描述
在这里插入图片描述
从源码中可以看到,readHolds本质上是一个ThreadLocalHoldCounter对象,并且继承了ThreadLocal,并且泛型是HoldCounter,所以ThreadLocalHoldCounter拥有ThreadLocal的功能,而ThreadLocal有存储功能,可以将元素存储在ThreadLocalMap中

下面就来看看get方法是做什么的

readHolds.get

在这里插入图片描述
可以看到,这个方法来自ThreadLocal源码如下

    public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //如果ThreadLocalMap不为空
        if (map != null) {
            //根据当前ThreadLocal去获取存储的键值对
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                //如果键值对不为空,返回value
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果ThreadLocalMap为空,需要进行初始化
        return setInitialValue();
    }

从这里大概就可以看到大致的模型了

当前线程的ThreadLocalMap中存储了以ThreadLocalHoldCounter的父类ThreadLocal为键的,HoldCounter为值的键值对,键是每个线程都能拿到的,但这里能进行区分是因为每个线程都存在了自己的ThreadLocalMap中,这样就进行区分了

在这里插入图片描述
在这里插入图片描述
可以看到,其底层其实是一个Entry数组

setInitialValue方法

这个方法其实就是当ThreadLocalMap没有创建的时候,需要进行初始化的时候调用

    */
    private T setInitialValue() {
        //获取默认值
        //默认值为null
        T value = initialValue();
        //获取当前线程
        Thread t = Thread.currentThread();
        //再次判断有没有创建
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //如果已经创建,直接set方法改变里面的entry属性
            map.set(this, value);
        else
            //如果没创建,就进行创建
            createMap(t, value);
        return value;
    }

在这里插入图片描述

set方法

当count属性为0时,会调用set方法,这个方法其实是用来修改值的或者为空时进行初始化的,没太看懂这个是干嘛的。。是为0时还没有进入缓存吗?

在这里插入图片描述
最后初始化缓存和容器了之后,就进行对缓存的count属性进行自增1了,这样就对应记录了每个线程的重入次数了

假如第一次获取失败

第一次获取失败是会执行fullTryAcquireShared方法的,这个方法名也挺有讲究,翻译为完全尝试,前面先尝试,尝试失败就进行完全尝试

源码如下

        final int fullTryAcquireShared(Thread current) {
            /*
             * This code is in part redundant with that in
             * tryAcquireShared but is simpler overall by not
             * complicating tryAcquireShared with interactions between
             * retries and lazily reading hold counts.
             */
            HoldCounter rh = null;
            //死循环
            //已经在开始自旋获取锁了
            for (;;) {
                //获取锁的状态量
                int c = getState();
                //获取写锁的状态量
                if (exclusiveCount(c) != 0) {
                    //并且判断,如果有人获取了写锁,并且该写锁的拥有者不是自己,返回-1
                	//交由AQS去实现共享锁了
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                    // else we hold the exclusive lock; blocking here
                    // would cause deadlock.
                }
                //如果没人获取写锁,或者写锁的拥有者是自己
                //判断获取读锁需不需要进行阻塞
                //这个方法对于公平锁和非公平锁有不同实现
                //为什么要判断要阻塞了,这就涉及到公平与非公平的性质
                //公平的话,肯定是要进行排队的,非公平的话不需要进行排队
                else if (readerShouldBlock()) {
                    // Make sure we're not acquiring read lock reentrantly
                    //如果进来需要进行阻塞,也就意味着需要进行排队,即前面已经有线程等待了
                    //这里是保证当前线程没有以可重入的方式获取锁的
                    //因为如果是以可重入的方式去获取锁,那就没有什么插不插队的概念了
                    //就比如买票看电影一样,你已经排队买了电影票了,然后有事离开了一下,那么下一次进来还需要排队买票吗?
                    //判断firstReader是不是当前线程,如果是就不用清除了
                    //因为第一个获取读锁的线程由ReentrantReadWriteLock维护
                    if (firstReader == current) {
                        //并且如果是第一个线程获取的,那就代表发生可重入了,没有插队概念
                        //不做任何处理,继续获取锁
                        // assert firstReaderHoldCount > 0;
                    } else {
                        //如果第一个获取读锁的线程不是自己,那就代表有其他线程已经拿到锁了
                        //下面就需要进行判断,当前线程获取读锁需不需要进行排队
                        if (rh == null) {
                            //获取缓存,即前一次获取锁的线程
                            rh = cachedHoldCounter;
                            //如果为空或者缓存的线程ID不为自己
                            //代表了
                            if (rh == null || rh.tid != getThreadId(current)) {
                                //获取自己ThreadLocal存储的共享锁状态量
                                rh = readHolds.get();
                                //如果为0.清空掉
                                if (rh.count == 0)
                                    //清空掉当前线程的ThreadLocal的读锁状态量
                                    readHolds.remove();
                            }
                        }
                        //为什么此时rh.count仍然为0就return????
                        //因为进入到这里代表了线程需要进行阻塞,而阻塞是交由AQS决定的
                        //如果是第一次获取锁,就代表没有发生可重入,那就需要进行排队了
                        //返回-1,让AQS去实现阻塞排队
                        if (rh.count == 0)
                            return -1;
                    }
                }
                //判断现在读锁的状态量是不是已经为最大值了
                if (sharedCount(c) == 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 {
                        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; // cache for release
                    }
                    //获取锁成功就进行return
                    return 1;
                }
                //获取锁失败,就进行下一轮循环
            }
        }

可以看到,这个方法其实是在CAS获取读锁的基础上,添加了阻塞功能,也就是说完整的尝试获取锁,不仅是判断写锁的状态和读锁的状态,还需要去考虑是否要阻塞

而考虑这个阻塞问题,其实就是关于公平锁和非公平锁的判断了,这里要注意一点的就是可重入锁是没有排队这个概念的!!!!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值