源码解读 | Java中ReentrantReadWriteLock的实现原理

本文将介绍Java中ReentrantReadWriteLock的实现原理,从JDK源码层面讲解读写锁的加锁、释放锁的流程,最后对流程进行总结。

读写锁概述

读写锁 ReentrantReadWriteLock 的依赖关系如下图所示

读写锁的基本使用如下

 

java

代码解读

复制代码

ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); readLock.lock(); readLock.unlock(); ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); writeLock.lock(); writeLock.unlock();

读锁和写锁使用同一个 Sync 同步器,即使用同一个等待队列和 state。读锁状态使用 state 高 16 位存储,写锁状态使用 state 低 16 位存储。

读锁涉及两个重入计数:state(高 16 位)用于记录有多少个线程持有该读锁,HoldCounter 用于记录当前线程重入该读锁多少次,每个线程均有一个对应的 HoldCounter 对象。

写锁涉及一个重入计数:state(低 16 位)用于记录有多少个线程持有该写锁。

读锁不支持条件变量,写锁支持条件变量。

写锁加锁流程

WriteLock 的 lock() 方法会调用同步器的 acquire()方法。

整理了一份Java面试题。包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

 需要全套面试笔记的【点击此处】即可免费获取

java

代码解读

复制代码

// WriteLock implements Lock, public void lock() { sync.acquire(1); }

同步器的 acquire() 方法会先调用 tryAcquire() 方法尝试获取写锁,获取写锁失败则调用 AbstractQueuedSynchronizer 的全参 acquire() 方法将线程加入等待队列。

 

java

代码解读

复制代码

// AbstractQueuedSynchronizer public final void acquire(int arg) { if (!tryAcquire(arg)) // 获取写锁失败则将线程加入等待队列 acquire(null, arg, false, false, false, 0L); }

在介绍 ReentrantLock 的实现原理时,已对 AbstractQueuedSynchronizer 的全参 acquire() 方法进行了较为详细的介绍,可参照 源码解读 | Java中ReentrantLock的实现原理,此处不再赘述,重点来看一下 tryAcquire() 方法如何获取写锁。

 

java

代码解读

复制代码

// Sync extends AbstractQueuedSynchronizer @ReservedStackAccess protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); // 写锁状态 int w = exclusiveCount(c); // 已加写锁或者读锁 if (c != 0) { if ( // 已加读锁:读写互斥,不能再加写锁 w == 0 || // 已加写锁,但不是自己加的(非写锁重入):写写互斥,不能再加写锁 current != getExclusiveOwnerThread() ) // 加写锁失败 return false; // 已加写锁且是自己加的 // 整数溢出 if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); // 写锁重入 setState(c + acquires); return true; } // 未加锁 if ( // 写锁应该等待 writerShouldBlock() || // 尝试获取写锁失败 !compareAndSetState(c, c + acquires) ) // 加锁失败 return false; // 加锁成功 // 将锁的持有者置为当前线程 setExclusiveOwnerThread(current); return true; }

NonfairSync 和 FairSync 对 writerShouldBlock() 有不同的实现。对于非公平锁,当前写线程可直接参与竞争,不应该等待,竞争失败才加入等待队列。对于公平锁,当前写线程不应该直接参与竞争,应该等待,加入等待队列。

 

java

代码解读

复制代码

// NonfairSync extends Sync final boolean writerShouldBlock() { // 直接参与竞争,不等待 return false; } // FairSync extends Sync final boolean writerShouldBlock() { // 等待队列中是否存在未取消的等待线程 // 存在未取消的线程则需要等待 return hasQueuedPredecessors(); }

读锁加锁流程

ReadLock 的 lock() 方法会调用同步器的 acquireShared() 方法。

 

java

代码解读

复制代码

// ReadLock implements Lock public void lock() { sync.acquireShared(1); }

同步器的 acquireShared() 方法会先调用 tryAcquireShared() 方法尝试获取读锁,获取读锁失败则调用 AbstractQueuedSynchronizer 全参的 acquire() 方法将线程加入等待队列。

 

java

代码解读

复制代码

// AbstractQueuedSynchronizer public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) // 获取读锁失败则将线程加入等待队列 acquire(null, arg, true, false, false, 0L); }

在介绍 ReentrantLock 的实现原理时,已对 AbstractQueuedSynchronizer 的全参 acquire() 方法进行了较为详细的介绍,可参照 ReentrantLock 原理,此处不再赘述,重点来看一下 tryAcquireShared() 方法如何获取写锁。

 

java

代码解读

复制代码

// Sync extends AbstractQueuedSynchronizer @ReservedStackAccess protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); if ( // 已加写锁 exclusiveCount(c) != 0 && // 写锁不是自己加的 getExclusiveOwnerThread() != current ) // 写读互斥,获取锁失败 return -1; // 读锁状态 int r = sharedCount(c); if ( // 读锁不应该等待 !readerShouldBlock() && // 未越界 r < MAX_COUNT && // 尝试获取读锁成功 compareAndSetState(c, c + SHARED_UNIT) ) { // 以下三个if-else分支用于读锁重入计数,忽略不影响理解主要加锁解锁流程 // 未加读锁 if (r == 0) { // 第一个获取读锁的线程 firstReader = current; firstReaderHoldCount = 1; } // 第一个获取读锁的线程重入 else if (firstReader == current) { firstReaderHoldCount++; } // 已加读锁且当前线程非读重入 else { // cachedHoldCounter缓存最近(上一个)获取读锁的线程的计数器 // rh:recent HoldCounter HoldCounter rh = cachedHoldCounter; if ( // 上一个获取读锁的线程为null rh == null || // 上一个获取读锁的线程不是当前线程 rh.tid != LockSupport.getThreadId(current) ) // 获取当前线程的计数器并缓存 cachedHoldCounter = rh = readHolds.get(); else if ( // 上一个获取读锁的线程为当前线程且计数为0(已被移除) rh.count == 0 ) // 重新将当前线程放入哈希表记录 readHolds.set(rh); // 更新计数(计数加1) rh.count++; } return 1; } // 读线程应该等待或者尝试获取锁失败或者越界 // 再尝试获取读锁 return fullTryAcquireShared(current); }

NonfairSync 和 FairSync 对 readerShouldBlock() 有不同的实现。对于非公平锁,只要等待队列第一个等待线程不是写线程,那么当前读线程就可直接参与竞争,不应该等待,竞争失败才加入等待队列。对于公平锁,当前读线程不应该直接参与竞争,应该等待,加入等待队列。

 

java

代码解读

复制代码

// NonfairSync extends Sync final boolean readerShouldBlock() { // 等待队列第一个等待线程是否为写线程 // 若为写线程,则读线程需要等待 // 不直接返回false是为避免写线程无限等待 return apparentlyFirstQueuedIsExclusive(); }

 

java

代码解读

复制代码

// FairSync extends Sync final boolean readerShouldBlock() { // 等待队列中是否存在未取消的等待线程 // 存在未取消的线程则需要等待 return hasQueuedPredecessors(); }

在 AbstractQueuedSynchronizer 的 全参acquire() 方法中,如果唤醒的是读线程,会调用 tryAcquireShared() 方法尝试获取锁,获取成功后,如果下一个等待线程是读线程,则会唤醒下一个等待线程。

 

java

代码解读

复制代码

// 如果读线程被唤醒,则调用 tryAcquireShared() 方法尝试获取锁 if (shared) acquired = (tryAcquireShared(arg) >= 0);

 

java

代码解读

复制代码

// 如果读线程被唤醒并获取锁成功,并且下一个等待线程仍为读线程,则由当前读线程直接去唤醒下一个等��线程 if (acquired) { ... if (shared) signalNextIfShared(node); ... }

 

java

代码解读

复制代码

// AbstractQueuedSynchronizer private static void signalNextIfShared(Node h) { Node s; // 如果下一个等待线程为读线程,则唤醒下一个线程 if (h != null && (s = h.next) != null && (s instanceof SharedNode) && s.status != 0) { s.getAndUnsetStatus(WAITING); LockSupport.unpark(s.waiter); } }

写锁释放锁流程

WriteLock 的 unlock() 方法会调用同步器的 release()方法

 

java

代码解读

复制代码

// WriteLock public void unlock() { sync.release(1); }

同步器的 release() 方法会先调用 tryRelease() 方法尝试释放写锁,释放成功后将唤醒下一个线程。

 

java

代码解读

复制代码

// AbstractQueuedSynchronizer public final boolean release(int arg) { if (tryRelease(arg)) { signalNext(head); return true; } return false; }

公平锁和非公平锁释放写锁的流程相同。

 

java

代码解读

复制代码

// Sync extends AbstractQueuedSynchronizer @ReservedStackAccess protected final boolean tryRelease(int releases) { // 写锁的持有者才能释放写锁 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); // 更新重入次数 int nextc = getState() - releases; boolean free = exclusiveCount(nextc) == 0; // 重入次数为0则将写锁的持有者置为null if (free) setExclusiveOwnerThread(null); setState(nextc); // 重入次数为0时认为写锁释放成功 return free; }

读锁释放流程

ReadLock 的 unlock() 方法会调用 同步器的 releaseShared() 方法。

 

java

代码解读

复制代码

// ReadLock implements Lock public void unlock() { sync.releaseShared(1); }

同步器的 releaseShared() 方法会先调用 tryReleaseShared() 方法尝试释放锁,释放成功后则会唤醒下一个等待线程。

 

java

代码解读

复制代码

// AbstractQueuedSynchronizer public final boolean releaseShared(int arg) { // 尝试释放读锁 if (tryReleaseShared(arg)) { // 唤醒下一等待线程 signalNext(head); return true; } return false; }

公平锁和非公平锁释放读锁的流程相同。注意到,释放读锁时使用循环反复尝试,因为读锁是共享的,可被多个线程持有。

 

java

代码解读

复制代码

// Sync extends AbstractQueuedSynchronizer protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); // ...修改线程重入计数... // 尝试释放锁 for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; if (compareAndSetState(c, nextc)) // 重入次数为0时认为读锁释放成功 return nextc == 0; } }

读写锁流程总结

写锁加锁流程

获取写锁失败的情况

  • 已加读锁,读写互斥;
  • 已加写锁且非重入,写写互斥;
  • 公平锁:等待队列存在未取消的线程。

获取写锁前必须释放读锁:

若线程 t 加读锁后,在释放读锁前,又加写锁,由于已加读锁,写锁获取失败,线程 t 陷入等待,又导致读锁无法释放,从而产生死锁。

读锁加锁流程

获取读锁失败的情况

  • 已加写锁非重入,写读互斥;
  • 非公平锁:等待队列中第一个等待线程为写线程;
  • 公平锁:等待队列存在未取消的线程。

读线程被唤醒后,如果等待队列下一个等待线程为读线程,则会直接将其唤醒。

写锁释放流程

尝试释放写锁,释放成功后将下一个等待线程唤醒。

如果线程不是写锁的持有者而去尝试释放写锁,则会抛异常 IllegalMonitorStateException。

 

java

代码解读

复制代码

// Sync extends AbstractQueuedSynchronizer // tryRelease() // if (!isHeldExclusively()) throw new IllegalMonitorStateException();

仅当写锁重入计数为 0 时,才认为写锁释放成功。

读锁释放流程

尝试释放读锁,释放成功后将下一个等待线程唤醒。

如果线程不是读锁的持有者而去尝试释放读锁,则会抛异常 unmatchedUnlockException。

 

java

代码解读

复制代码

// Sync extends AbstractQueuedSynchronizer // tryReleaseShared() if (count <= 0) // 重入计数小于等于0说明当前线程未持有读锁 throw unmatchedUnlockException();

仅当读锁重入计数为 0 时,才认为读锁释放成功。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值