ReentrantReadWriteLock

382 阅读4分钟

是什么

读写锁

读写锁什么效果

  • 读读并行
  • 读写串行
  • 写写串行
  • 所以只要有写的就会串行
  • 关于重入锁,读读可以重入
  • 关于重入锁,写写可以重入
  • 关于重入锁,写读可以重入
  • 关于重入锁,读写不可以重入,这个是为什么,因为假如读写可以重入,可能会造成死锁。

底层怎么区分读锁和写锁,是有两个锁嘛

    public ReentrantReadWriteLock(boolean fair) {
    //默认 非公平锁
        sync = fair ? new FairSync() : new NonfairSync();
    //ReadLock 读锁
        readerLock = new ReadLock(this);
    //WriteLock 写锁    
        writerLock = new WriteLock(this);
    }
  • 实体类分为读锁和写锁
  • 所以读写锁是两个锁嘛?

写锁的lock方法

调用syncacquiresync是对象ReentrantReadWriteLock的属性

        public void lock() {
            sync.acquire(1);
        }

acquire

来到 aqsacquire,发现跟ReentrantLock一样,但是里面的实现不一样了

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
tryAcquire

来到 ReentrantReadWriteLock内部类SynctryAcquire来尝试获取锁

 protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            //跟 ReentrantLock 一样 state 表示锁标识,0标识 未被加锁,大于0表示有被加锁
            //不一样的是 state 高16位标识读锁,低16位标识写锁
            int c = getState();
            //1.exclusiveCount 里面的逻辑其实就是取 c 高位的值,具体做法如下
            //2.c & ((1 << SHARED_SHIFT) - 1
            //3.(1 << 16) - 1
            //    0000 0000 0000 0000 0000 0000 0000 0001   
            //<<  0000 0000 0000 0001 0000 0000 0000 0000   
            //-1  0000 0000 0000 0000 1111 1111 1111 1111   
            //4.c & 0000 0000 0000 0000 1111 1111 1111 1111  
            //    **** **** **** **** **** **** **** ****
            //&   0000 0000 0000 0000 1111 1111 1111 1111   
            //   0000 0000 0000 0000 **** **** **** ****  
            //5.通过上面的做法就可以把c低位取出,也就是写锁的标志
            int w = exclusiveCount(c);
            //如果 c 不为零 说明有被加锁
            if (c != 0) {
                //1.如果 w等于零 说明没有写锁,没有写锁,但是被加锁了
                //说明有读锁,有读锁,立刻返回false,加锁失败,说明读写不能重入
                //2.如果w不等于零,说明有写锁,如果有写锁但是获取锁的线程等于当前线程
                //就可以重入,说明写写可以重入
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //重入锁,重入需要往低位加 acquires 判断是否大于十六位
                //如果大于十六位需要抛出异常
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //设置 state
                setState(c + acquires);
                return true;
            }
            //1.如果 c = 0的情况
            //2.writerShouldBlock 如果是非公平锁直接返回 false,如果是公平锁
            //就会去判断是否需要排队
            //3.如果 writerShouldBlock 返回 false 就通过 cas 
            //去获取锁,获取失败,返回false
            //4.获取成功,设置当前线程为持有锁的线程
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

tryAcquire 总结

  • 1.tryAcquire 是要尝试去获取锁
  • 2.锁标志stateReentrantLock类似0标识未被上锁,不一样的是高16位表示读锁,低十六位表示写锁,所以读锁跟写锁用的是同一个字段标识,但是高位地位区分
  • 3.tryAcquire会先去判断c是否等于零,也就是是否被加锁了,如果被加锁了,就判断是否是被读锁加锁了,读锁加锁之后,写锁不能重入,如果是写锁加锁了,写锁可以重入,可以重入之后还会判断,加锁之后state的低位是否超过十六位,超过报异常,如果可以重入,就往c加一
  • 4.如果c等于零,也就是还未被加锁,如果是公平公锁会先去判断是否需要排队,如果是非公平锁就没有排队的判断,不需要排队,就通过cas去修改ccas成功就会设置持有锁的线程为当前线程,加锁成功。

acquireQueued addWaiter

acquireQueuedaddWaiter的实现都跟ReentrantLock一样,加锁失败,把该节点放到队列排队等待被park

读锁的lock方法

调用syncacquireShared去获取共享锁,sync跟写锁的sync对象是同一个对象,所以其实读写锁是同一把锁,只是加锁和解锁的实现不太一样。

    public void lock() {
            sync.acquireShared(1);
        }

acquireShared

aqsacquireShared

   public final void acquireShared(int arg) {
        if (
        
        (arg) < 0)
            doAcquireShared(arg);
    }

tryAcquireShared


        protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            //1.exclusiveCount 获取排他锁也就是写锁标志  
            //2.如果写锁标志不为零,说明有写锁
            //2.getExclusiveOwnerThread() != current 有写锁,而且当前线程不等于持有锁的线程
            //返回-1 就是加锁失败
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            //sharedCount 是将c移动16位,即是获取高16位
            //高16位表示读锁的标志
            int r = sharedCount(c);
            //1.readerShouldBlock 判断读锁是否需要阻塞
            //2.r < MAX_COUNT 判断读锁加锁次数是否大于最大值
            //3.如果不需要阻塞,而且读锁加锁次数不大于加锁次数
            //就通过cas设置c大小,加锁,注意是往c高位加一
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                //r等于o说明目前没有读锁
                if (r == 0) {
                //设置第一个读锁的线程和读锁当前线程重入次数1
                    firstReader = current;
                    firstReaderHoldCount = 1;
                //如果有读锁,而且当前第一个读锁线程等于当前读锁线程
                //就往当前读锁线程重入次数加一
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                //1.cachedHoldCounter  缓存最近一次读锁
                //2.如果缓存读锁为空或者当前线程非最近一次读锁的,就从 readHolds 重新获取
                //3.往  HoldCounter 加一 
                //4.HoldCounter 是线程本地变量,存有线程的读锁重入次数
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                //返回加锁成功
                return 1;
            }
            //如果需要排队或者cas 失败,就进入等待循环获取锁
            return fullTryAcquireShared(current);
        }

tryAcquireShared 总结

  • 1.state高16位存的是读锁的标志
  • 2.会先判断是否加了写锁,而且持有锁的线程是否该线程,如果是就可以重入
  • 3.如果写锁可以重入或者是被读锁加锁了,也可以在加读锁
  • 4.读锁加锁首先会往state的高16位加一
  • 5.HoldCounter放到线程本地变量,里面的属性有count也就是某个线程加锁次数 还有tid,表示所属线程的id

写锁的unLock方法

其实写锁的unLock 方法跟 ReentrantLock的解锁方法一样的

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

读锁的 unLock 方法

releaseShared 释放共享锁

        public void unlock() {
            sync.releaseShared(1);
        }

releaseShared

aqsreleaseShared

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

tryReleaseShared(arg)

        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
			//如果第一个获取读锁的是当前线程就分为两种情况
			//一种 firstReaderHoldCount == 1 没有重入,直接将 firstReader = null
			//一种 firstReaderHoldCount != 1 有重入  firstReader -1
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
			//如果不是第一个获取锁的线程,那线程锁记录标志到 HoldCounter
			//获取到对应线程的 HoldCounter 然后对 HoldCounter 的value 减一
                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;
            }
			//以上两个 if 主要是对线程本地记录的获取锁次数的释放
			//下面这个for 循环是对 state 的释放锁
			//通过cas 修改 c 高16位的大小
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    // Releasing the read lock has no effect on readers,
                    // but it may allow waiting writers to proceed if
                    // both read and write locks are now free.
                    return nextc == 0;
            }
        }

tryReleaseShared 总结

  • 1.释放读锁的时候有三个点
  • 2.第一个点,会先去判断当前线程是否是 firstReader 也就是第一个获取锁 如果是就 firstReaderHoldCount 减一
  • 3.第二个点,如果当前线程不等于firstReader,就会从 HoldCounter 的 value 减一
  • 4.第三个点,自旋去 cas 修改state的高16位减一

总结

  • 1.ReentrantReadWriteLock读写锁是一个锁,都是 state 控制的,只是高16位控制读锁 低16位控制写锁
  • 2.写锁的加锁会先判断是否已经加锁了,如果已经加锁了,就会判断加的是否是写锁,如果写锁 可以重入,就往 state (低16位)加一,如果加的是读锁就不能重入加锁失败,如果没有加锁,就会就会通过 cas 去 修改 state 去尝试加锁,加锁失败就会将节点放到 aqs 队列中等待 unpark
  • 3.读锁加锁的时候会先判断有没有加写锁,如果有加写锁,又是当前线程,就可以重入,否则加锁失败。 如果没有加写锁,就可以加锁,加锁的过程有主要是数值加一,主要是 firstReaderHoldCount 和 HoldCounter 的 value 和 state (高16位)
  • 4.读锁被 unpark 之后会去调用 doReleaseShared 去尝试 unpark 其他被park的读锁,也就是 共享锁会去尝试唤醒共享锁的线程。