并发系列(十三)-----ReentrantReadWriteLock源码解析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/MyPersonalSong/article/details/84871077

一 简介

读写锁如何正确的使用在上一篇的文章中简单的介绍了,并且已经知道读写锁内部维护了两个锁分别是读锁和写锁。下面是这两个锁的特点

1,写锁是独占的、排他的,当一个线程持有写锁是,其他任何线程都不允许获取到锁不管是读锁还是写锁。

2.读锁是共享的,允许多个线程同时访问资源。

3.一个线程同时既可以持有读锁也可以写锁,获取顺序是先获取写锁后是可以获取读锁的这种现象较锁降级,但是不可以先获取读锁然后在获取写锁那样会造成死锁,这种又叫锁升级.

二 ReentrantReadWriteLock组成

ReentrantReadWriteLock内部构成比较多,下面是UMl图

对上图做一个简单的说明

虚线箭头 表示实现了接口 例如ReentrantReadWriteLock实现了ReadWriteLock

实现箭头 表示继承了某个类 例如FairSync和NofairSync继承了sync

加号线     表示组成    例如sync由内部类HoldCounter和ThreadLocalHoldCounter构成

更据上图我们可以知道ReentrantReadWriteLock由5个内部类构成分别是Sync,FairSync,NofairSync,WriteLock,ReadLock。Sync内部也维护了两个类分别是HoldCounter和ThreadLocalHoldCounter两个类,其中ThreadLocalHoldCounter继承ThreadLcoal。这两个类是用来服务于读锁的重入的。FairSync和NofairSync是Sync的两个不同的实现版本。WriteLock和ReadLock继承Lock接口。

三 实现原理及源码解析

ReentrantReadWriteLcok的是实现还是基于AQS的,其中写锁利用的是AQS中的独占模式读锁利用的是AQS中的共享模式。

1.Sync中资源解析

我们知道AQS中的资源是一个int类型的数值,一个资源它怎么即表示读锁有表示写锁的呢?我们知道int类型在Java中的大小是32bit,那么int类型的二进制最大值就是1111 1111 1111 1111 1111 1111 1111 1111。读写利用将他将的高16位表示用来表示读锁,低16位表示的是写锁。例如 0000 0000 0000 0000 0000 0000 0000 0001 就表示有写锁存在,0000 0000 0000 0001 0000 0000 0000 0000,就表示有读锁存在。0000 0000 0000 0001 0000 0000 0000 0001表示既有读锁存在也有写锁的存在。

下面是源码

    /**
         * 对32位的int进分割
         */
        static final int SHARED_SHIFT = 16;
        /**
         * 十进制为65536 移位运算的https://zhuanlan.zhihu.com/p/30108890 
         */
        static final int SHARED_UNIT = (1 << SHARED_SHIFT);
        /**
         * 65535 0000 0000 0000 0000 1111 1111 1111 1111
         *最大资源数
         */
        static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
        /**
         * 65535 0000 0000 0000 0000 1111 1111 1111 1111 
         * 用来计算独占资源
         */
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

    /**
         * 计算readLock的获取次数包含重入的次数
         *
         * @param c 资源数
         * @return 计算好的次数
         */
        static int sharedCount(int c) {
            //将字节向左无符号右移16位只剩下原来的高16位
            //eg  c = <0000 0000 0000 1111> 0000 0000 0000 0011
            //移位后为0000 0000 0000 0000 <0000 0000 0000 1111> 
            return c >>> SHARED_SHIFT;
        }

        /**
         * 计算writeLock的获取次数包括重入的次数
         *
         * @param c 资源数
         * @return 计算后的独占资源数
         */
        static int exclusiveCount(int c) {
            //如果这个c小于65536时若&上65535时就剩下低16位,还是C
            //eg 0000 0000 0000 1111 0000 0000 0000 0011
            //相&后 0000 0000 0000 0000 <0000 0000 0000 0011>
            return c & EXCLUSIVE_MASK;
        }

上面的源码涉及到了二进制的一些运算,这个大学都应该学过,关于位运算在注解中我给出一篇博客可以自己去学习。

2.写锁的实现

AQS中的独占模式对应着读写锁的写锁,而且我们也已经知道int类型的低16位时用来表示独占模式的,下面我们先看写锁获取的源码。


        /**
         * 写锁资源的获取
         *
         * @param acquires 资源数
         * @return 获取是否成功true表示成功false表示失败
         */
        @Override
        protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            //获取当前资源
            int c = getState();
            //获取当前资源低16位的值
            int w = exclusiveCount(c);
            if (c != 0) {
                //如果c != 0表表示有线程在持有锁这个锁可能是读锁也可能是写锁
                if (w == 0 || current != getExclusiveOwnerThread()) {
                //如果低16位为0,那么表示高16位有数字,那么就有读锁存在直接返回false添加到同步对列中
                //如果低16位不为0说明低16位有数字,那么就有写锁存在,如果写锁不是当前线程
                //所持有的那么直接返回false进入同步对列中去
                    return false;
                }
              
                if (w + exclusiveCount(acquires) > MAX_COUNT) {
                    throw new Error("Maximum lock count exceeded");
                }
               //代码走到这里w!=0 且还是持有写锁的还是当前线程,资源数也有那么就要重入了
               //因为是独占模式,而且又是当前线程执行所以不用管考虑并发直接设置就可以
                setState(c + acquires);
                //返回true,获取锁成功
                return true;
            }
            //如果线程获取策略是阻塞,直接进入到同步对列中
            //如果线程获取策略不是阻塞,直接使用CAS获取资源注意这里有可能发生并发所以使用了CAS
            if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) {
                return false;
            }
            //走到这里说明compareAndSetState(c, c + acquires)执行成功也就是获取资源成功
            //设置线程
            setExclusiveOwnerThread(current);
            return true;
        }

上面的逻辑已经说明的听清楚的,只有一个方法需要说明writerShouldBlock() 这个方法是返回一个boolean值,这个布尔值用来决定线程是现在获取还是等一会获取。

上面的源码时写锁用来获取资源的,下面我们看一下写锁的资源时如何释放的。如下源码

        /**
         * 释放独占资源
         *
         * @param release 资源数
         * @return 写锁的资源全都的释放完成返回true
         */
        @Override
        protected final boolean tryRelease(int release) {
            //是不是当前线程
            if (!isHeldExclusively()) {
                throw new IllegalMonitorStateException();
            }
            int nextc = getState() - release;
            //获取低16位 如果低16位为0的话表示读锁没有了
            boolean free = exclusiveCount(nextc) == 0;
            if (free) {
                //如果独占资源为0的话将持有线程变为null
                setExclusiveOwnerThread(null);
            }
            //设置资源这这里不用考虑并发,写锁时独占模式
            setState(nextc);
            return free;
        }

源码的逻辑时比较简单的不再做过多的介绍。

3.读锁的实现

AQS中的共享模式对应着读锁的实现,再看源码的时候我们要看一下Sync中的几个成员变量,这几个成员变量是为读锁的重入服务的。下面是源码。

  /**
         * 几乎每个获取 readLock 的线程都会含有一个 HoldCounter 用来记录 线程 id 与 获取 readLock 的次数 ( writeLock 的获取是由
         * state 的低16位 及 aqs中的exclusiveOwnerThread 来进行记录) 这里有个注意点 第一次获取 readLock 的线程使用 firstReader ,
         * firstReaderHoldCount 来进行记录 (PS: 不对, 我们想一下为什么不 统一用 HoldCounter 来进行记录呢? 原 因: 所用的
         * HoldCounter 都是放在 ThreadLocal 里面, 而很多有些场景中只有一个线程获取 readLock 与 writeLock , 这种情况还用
         * ThreadLocal 的话那就有点浪费(ThreadLocal.get() 比直接 通过 reference 来获取数据相对来说耗性能))
         */
        static final class HoldCounter {
            /**
             * readLock的获取次数
             */
            int count = 0;
            /**
             * 线程的ID
             */
            final long tid = getThreadId(Thread.currentThread());
        }

        /**
         * 简单的自定义的 ThreadLocal 来用进行记录  readLock 获取的次数
         */
        static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
            @Override
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

        /**
         * readLock 获取记录容器 ThreadLocal(ThreadLocal 的使用过程中当 HoldCounter.count == 0 时要进行 remove ,
         * 不然很有可能导致 内存的泄露)
         */
        private transient ThreadLocalHoldCounter readHolds;
        /**
         * 最后一次获取 readLock 的 HoldCounter 的缓存 (PS: 还是上面的问题 有了 readHolds 为什么还需要 cachedHoldCounter呢?
         * 非常大的场景中, 这次进行release readLock的线程就是上次 acquire 的线程, 这样直接通过cachedHoldCounter来进行获取, 节 省了通过
         * readHolds 的 lookup 的过程)
         */
        private transient HoldCounter cachedHoldCounter;
        /**
         * 下面两个是用来进行记录 第一次获取 readLock 的线程的信息 准确的说是第一次获取 readLock 并且 没有 release 的线程, 一旦线程进行 release
         * readLock, 则 firstReader会被置位 null
         */
        private transient Thread firstReader = null;
        private transient int firstReaderHoldCount;

关于这几个类的作用,上面的注释应该很详细了。在往下我们看读锁的资源获取。

   /**
         * 读锁的获取
         *
         * @param unused 资源数
         * @return 大于0表示获取成功
         */
        @Override
        protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            //如果写锁存在的话当前线程也没有持有写锁的直接不允许获取读锁,添加到同步对列中
            if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) {
                return -1;
            }
            //获取高16位的数
            int r = sharedCount(c);
            //这里只是简单的去获取一下
            //查看等待的策略是否等待,不等待的话检查合法性在替换高16位的资源
            //这里使用了CAS存在并发
            if (!readerShouldBlock() && r < MAX_COUNT &&
                    compareAndSetState(c, c + SHARED_UNIT)) {
                //如果读锁为0的话那么说明是第一次获取
                if (r == 0) {
                    //设置线程和读锁次数
                    firstReader = current;
                    firstReaderHoldCount = 1;
                    //没有使用HoldCounter
                } else if (firstReader == current) {
                    //firstReader是当前线程的话直接次数加一
                    firstReaderHoldCount++;
                } else {
                    //不是的话获取HoldCounter
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        //如果为null的话或者不是当前线程时重新在获取一个
                        cachedHoldCounter = rh = readHolds.get();
                    } else if (rh.count == 0) {
                        readHolds.set(rh);
                    }
                    //次数加一
                    rh.count++;
                }
                //获取资源成功
                return 1;
            }
            //简单的获取不到,在进行完全获取。
            return fullTryAcquireShared(current);
        }

/**
         * 读锁的完全获取资源
         *
         * @param current 当前的线程
         * @return 大于等于0表示获取成功,小于0表示获取失败
         */
        final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            //for循环不停的获取资源
            for (; ; ) {
                //获取资源
                int c = getState();
                //当前有写锁存在的时候
                if (exclusiveCount(c) != 0) {
                    //如果读锁不是当前的线程那么直接结束循环返回-1;获取失败
                    if (getExclusiveOwnerThread() != current) {
                        return -1;
                    }
                } else if (readerShouldBlock()) {
                    //阻塞策略true表示要阻塞
                    if (firstReader == current) {
                        //判断当前线程是不是第一条
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                //获取一个新的
                                rh = readHolds.get();
                                //检查是否是持有读锁是否为0若为0的话,说明没有获取过
                                //直接移除
                                if (rh.count == 0) {
                                    readHolds.remove();
                                }
                            }

                        }
                        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)) {
                    //代码走到这里说明已经获取成功了
                    //如果高16位的资源数是0那么说明他是第一个
                    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;
                    }
                    return 1;
                }
            }
        }

读锁资源的释放,比较简单,就是操作读锁的次数,和修改资源,但是在修改资源的时候需要考虑线程安全,因为时并发访问。

下面时源码


        /**
         * 读锁的释放
         *
         * @param unused 释放资源数
         * @return 是否释放成功
         */
        @Override
        protected final boolean tryReleaseShared(int unused) {
            //获取当前线程
            Thread current = Thread.currentThread();
            //如果firstReader等于当前线程的话说明是要释放只有一条获取写锁的线程了
            if (firstReader == current) {
                //等于1的话说明只是获取读锁一次释放了也就需要将firstReader至为null
                if (firstReaderHoldCount == 1) {
                    firstReader = null;
                } else {
                    //不等于1说明获取读锁多次直接截减去1
                    firstReaderHoldCount--;
                }
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current)) {
                    rh = readHolds.get();
                }
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0) {
                        throw unmatchedUnlockException();
                    }
                }
                --rh.count;
            }
            for (; ; ) {
                //去更新资源更新高16位
                int c = getState();
                int nextc = c - SHARED_UNIT;
                //使用CAS去更新存在并发
                if (compareAndSetState(c, nextc)) {
                    return nextc == 0;
                }
            }
        }

四 锁升级的死锁分析

读写锁的锁升级就是,一个线程获取完读锁没有释放读锁,就去获取写锁。这样的或会遭成死锁。

1.一个线程获取到了读锁,那么资源数也就不等于0了。

2.在获取写锁时如果线程就会走到下面代码中。

  if (c != 0) {
                //如果c != 0表表示有线程在持有锁这个锁可能是读锁也可能是写锁
                if (w == 0 || current != getExclusiveOwnerThread()) {
                //如果低16位为0,那么表示高16位有数字,那么就有读锁存在直接返回false添加到同步对列中
                //如果低16位不为0说明低16位有数字,那么就有写锁存在,如果写锁不是当前线程
                //所持有的那么直接返回false进入同步对列中去
                    return false;
                }
              
                if (w + exclusiveCount(acquires) > MAX_COUNT) {
                    throw new Error("Maximum lock count exceeded");
                }
               //代码走到这里w!=0 且还是持有写锁的还是当前线程,资源数也有那么就要重入了
               //因为是独占模式,而且又是当前线程执行所以不用管考虑并发直接设置就可以
                setState(c + acquires);
                //返回true,获取锁成功
                return true;
            }

3.当走到2的代码时候就会返回一个fasle,因为当前线程是是没有持有读锁的。那么线程就会添加到AQS中的同步队列中线程就会waiting。

4.线程如果waiting状态中读锁就不可能被释放了,线程永远处于waiting状态中,造成了死锁。

五 总结

1.资源高16为表示读锁,低16位表示写锁,通过位运算和&运算实现

2.写锁对应着AQS中的独占模式,读锁对应着AQS中的共享模式

3.锁升级造成的死锁

猜你喜欢

转载自blog.csdn.net/MyPersonalSong/article/details/84871077