既然有了锁,为什么还需要读写锁?我们来想象下这个场景。你们小区楼下有个公告栏,有时候有人会写个招租,有时候有人会写个寻物启事…… 当然一个人正在改公告栏的时候,另外一个人就不能同时改了,这里就相当于有了一把无形的锁,我改的时候就把广告栏“锁住”,改完再“解锁”,当然别人锁住了之后我也改不了。说完了“写”再说“读”,一个人在读公告栏的时候,别人就不能去写了,这样不礼貌,这里也相当于读的人用一把“锁”把公告栏给锁了。


如果这里读者用的锁和写者用的锁是一样的,那么这把锁不紧不然别人写了,也不让别人读了,相当于一个人在看公告栏,别人就不能看了,这明显不合理啊。 所以要把读和写用的锁区分开来,所有读的人共享一把锁,写的人独享锁。放到公告栏的例子上,改公告的时候同时只有一个人可以看,但读的时候所有人可以同时读,这样就可以把“公告栏”这个资源的利用率最大化。


看到这里,你应该已经理解了什么叫做“读写锁”,接下来我们直接看下jdk中ReentrantReadWriteLock的实现,再次建议先阅读ReentrantLock的具体实现。

                


从类结构图看,貌似它比ReentrantLock更复杂写,多两个内部类 ReadLock 和 WriteLock,看着Lock提供的api完全一样,看来得从具体实现上来看其二者有什么样的差异了。


    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

从ReentrantReadWriteLock的构造方法可以看出,它也支持公平锁和非公平锁,当然默认也是非公平锁。和ReentrantLock一样,加锁和解锁的实现逻辑都是在 Sync 里,所以我们重点看下Sync的实现,代码太多这里就不贴完整代码了,建议读者自行打开代码。

                

Sync

从Sync的类结构图来看,它还是相当复杂的,别急让我们来捋一捋,我们先从WriteLock看起(看起来会比较熟悉),看下他的lock和release的具体实现。


@ReservedStackAccess
final boolean tryWriteLock() {
	Thread current = Thread.currentThread();   // 1
	int c = getState();    // 2
	if (c != 0) {    // 3 
		int w = exclusiveCount(c);   // 4
		if (w == 0 || current != getExclusiveOwnerThread())  // 5
			return false;
		if (w == MAX_COUNT)   //6.  MAX_COUNT = 65535
			throw new Error("Maximum lock count exceeded");
	}
	if (!compareAndSetState(c, c + 1))  // 7
		return false;
	setExclusiveOwnerThread(current);  // 8
	return true;
}



如果你看过ReentrantLock的话,相信这段代码你已经完全能看懂了。这里我再大概说下这段代码的流程


1、获取到当前线程。

2、获取到锁对象的state值,state是保存了锁的状态。

3、如果state不为0,说明已经有线程加过锁了,这时候需要额外判断下,跳到4。 如果state为0,直接跳到 7。

4、获取到当前加写锁的次数,这里获取的是state的低16位。

5、c已经不为0了,如果w不为0说明有线程加了写锁,如果加了写锁的线程也不是当前线程的,加锁就失败了。

6、这里需要额外判断下锁重入的次数,如果已经到65535就不能再加锁了,后续会解释为什么是65535。

7、执行CAS操作更改锁状态 state。

8、到这里说明加写锁已经成功了,把当前锁的持有者记录下来。

 

@ReservedStackAccess
final boolean tryReadLock() {
	Thread current = Thread.currentThread();   // 1
	for (;;) {
		int c = getState();  // 2
		if (exclusiveCount(c) != 0 &&
			getExclusiveOwnerThread() != current)  // 3 
			return false;
		int r = sharedCount(c);  // 4
		if (r == MAX_COUNT)   // 5
			throw new Error("Maximum lock count exceeded");
		if (compareAndSetState(c, c + SHARED_UNIT)) {  // 6
			if (r == 0) {
				firstReader = current;
				firstReaderHoldCount = 1;
			} else if (firstReader == current) {
				firstReaderHoldCount++;
			} else {
				HoldCounter rh = cachedHoldCounter;
				if (rh == null ||
					rh.tid != LockSupport.getThreadId(current))
					cachedHoldCounter = rh = readHolds.get();
				else if (rh.count == 0)
					readHolds.set(rh);
				rh.count++;
			}
			return true;
		}
	}
}


读锁的加锁代码就完全不一样了,第一眼看到的不同就是这里有个大大的无限循环,我们还是来看下读锁的加锁过程。


1、获取当前线程。

2、获取锁的state状态值。

3、如果写锁的加锁次数不是0切写锁持有者不是当前线程,加读锁失败。

4、获取读锁的加锁次数,sharedCount©获取的是state的高16位。

5、如果读锁加锁次数达到65535,抛Error,和写锁一样,只能加65535次。

6、执行到这,说明可以加锁,使用CAS更新state成功后这里就开始记录一些读锁的状态信息,注意这里state增加值不是1,而是SHARED_UNIT(65536)。


看完readLock和writeLock的加锁方式就可以大体理解ReentrantReadWriteLock的实现了,原来它只是把ReentrantLock中的state分成两部分来用,高16位记录读锁状态,低16位记录写锁状态,如下图。

                

这也是为什么上文中加锁最大次数是65535的原因了,这也是而是SHARED_UNIT的值为65536的原因。


理解了加锁的代码,解锁部分也就好理解了,本质上是把加锁的代码反向执行下,代码如下。


   @ReservedStackAccess
        protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }
        
        @ReservedStackAccess
        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null ||
                    rh.tid != LockSupport.getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            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;
            }
        }

Sync中还有一个ThreadLocalHoldCounter类,这个类的作用其实是记录每个线程对读锁的加锁测试,见名知意线程级的统计,代码也很简单,这里就不再贴了。

Sync中除了上文说到的几个加解锁的API,其余一些API就是获取Sync对象中各个状态的API,没什么好说的。


FairSync & NonfairSync

说完了抽象类Sync,我们来说下它的两个具体实现 FairSync 和 NonfairSync。 这两个实现类非常非常简单,只是重写了 writerShouldBlock() 和 readerShouldBlock() 方法而已,如果你已经知道什么是公平和非公平了,这地方也就很好理解了。

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        final boolean writerShouldBlock() { 
        // 写锁可以始终不被等待队列里的线程阻塞,只要当前锁是未锁定状态就可以加锁 
            return false;
        }
        final boolean readerShouldBlock() {  
        //这个方法判断队列的head.next是否正在等待写锁,这个方法确保读锁不应该让写锁始终等待,即便是非公平的,但写锁有更高的优先级,获取读锁还是得排队。
            return apparentlyFirstQueuedIsExclusive();
        }
    }
    // 公平锁就很好理解了,只要等待队列不为空,就得去排队  
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }

ReadLock & WriteLock

                                    

                        

其实看完Sync里的逻辑,基本上ReadLock和WriteLock的实现逻辑我们已经知道了。ReadLock和WriteLock只是向用户提供里有些功能抽象(实现了Lock中的方法),封装好了具体的实现,其实具体逻辑还是在Sync中实现。


从类继承关系来看,二者也只是简单


结论

了解完ReentrantReadWriteLock的实现后你就会发现,它其实和ReentrantLock一样,之前把ReentrantLock中的state切分成两部分用,高16位作为读锁的state,低16位作为写锁。如果把ReadLock和WriteLock拉出来单独看的话,二者都是一个ReentrantLock,只是不能像ReentrantLock那样重入那么多次而已。


ReentrantReadWriteLock的出现大幅提升了多读少写场景下的性能问题,但它依旧有自己的缺点,就是它可能会导致写饥饿。还是拿小区公告栏的例子,如果任意时刻都有人在看公告栏,你也不好打断人家所以你公告更新不了啊,所以想更新的人就得一直等着。

关注我,下次和大家一起看下 StampedLock 是如何解决饥饿问题的。


来源:https://blog.csdn.net/xindoo/article/details/104161776