看一下读写锁具体实现tryReleaseShared 的方法
protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); //1,更新或者移出线程内部计数器的值 if (firstReader == current) { //当前线程是第一个获取读锁的线程 if (firstReaderHoldCount == 1) //直接置空 firstReader = null; else //该线程获取读锁重入多次,计数器-1 firstReaderHoldCount--; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) rh = readHolds.get(); int count = rh.count; if (count <= 1) { //非第一个获取读锁线程,避免ThreadLocal内存泄漏,移出计数器 readHolds.remove(); if (count <= 0) //此处是调用释放锁次数比获取锁次数还多情况,直接抛异常 throw unmatchedUnlockException(); } --rh.count; } //2,循环cas更新同步锁的值 for (;;) { int c = getState(); //读锁同步状态-1 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. //返回完全释放读锁,读锁值是否==0,完全释放,等待写锁线程可获取 return nextc == 0; } }tryReleaseShared 返回true情况,表示完全释放读锁,执行doReleaseShared,那就需要唤醒同步队列中等待的其他线程
在读写锁中存在几种情况
情况一、如果当前获取锁的线程占用的是写锁,则后来无论是获取读锁还写锁的线程都会被阻塞在同步队列中,
同步队列是FIFO队列,在占用写锁的释放后,node1获取读锁,因读锁是共享的,继续唤醒后一个共享节点。
如上图,在node1获取到读锁时,会调用doReleaseShared方法,继续唤醒下一个共享节点node2,可以持续将唤醒动作传递下去,如果node2后面还存在几个等待获取读锁的线程,这些线程是由谁唤醒的?是其前置节点,还是第一个获取读锁的节点? 应该是第1个获取锁的节点,这里即node1, 由下代码可见,在无限循环中,只有头节点没有变化时,即再没其他节点获取到锁后,才会跳出循环。
private void doReleaseShared() { for (;;) { //获取同步队列中头节点 Node h = head; //同步队列中节点不为空,且节点数至少2个 if (h != null && h != tail) { int ws = h.waitStatus; //1,表示后继节点需要被唤醒 if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases //唤醒后继节点 unparkSuccessor(h); } //2,后继节点暂时不需要唤醒,设置节点 ws = -3, 确保后面可以继续传递下去 else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } //如果头节点发生变化,表示已经有其他线程获取到锁了,需要重新循环,确保可以将唤醒动作传递下去。 if (h == head) // loop if head changed break; } } 5、思考1、在非公平获取锁方式下,是否存在等待获取写锁的线程始终获取不到锁,每次都被后来获取读锁的线程抢先,造成饥饿现象?
存在这种情况,从获取读锁源码中看出,如果第一个线程获取到读锁正在执行情况下,第二个等待获取写锁的线程在同步队列中挂起等待,在第一个线程没有释放读锁情况下,又陆续来了线程获取读锁,因为读锁是共享的,线程都可以获取到读锁,始终是在读锁没有释放完毕加入获取读锁的线程,那么等待获取写锁的线程是始终拿不到写锁,导致饥饿。为什么默认还是非公平模式?因为减少线程的上下文切换,保证更大的吞吐量。
6、总结1、读写锁可支持公平和非公平两种方式获取锁。
2、支持锁降级,写锁可降级为读锁,但读锁不可升级为写锁。
3、大多数场景是读多于写的,所以ReentrantReadWriteLock 比 ReentrantLock(排他锁)有更好的并发性能和吞吐量。
4、读写锁中读锁和写锁都支持锁重入。