真实面经题目 · 原创解析

读写锁怎么实现?

读写锁的核心是把访问分成共享读和独占写:多个读线程可以同时进入,写线程进入时必须排斥所有读线程和其他写线程。Java 的 ReentrantReadWriteLock 基于 AQS 实现,用同一个 state 同时记录读锁和写锁计数,并支持可重入、公平策略、锁降级等语义。

出现于:阿里巴巴 · 后端开发

60 秒回答模板

读写锁的实现思路是用两种锁状态表达两类访问权限:读锁是共享锁,只要当前没有写线程持有写锁,多个读线程就可以同时获取;写锁是独占锁,获取时要求没有其他读锁和写锁存在。以 Java 的 ReentrantReadWriteLock 为例,它基于 AQS,把 state 拆成高低两部分:高位记录读锁持有次数,低位记录写锁重入次数。读锁获取走共享模式,写锁获取走独占模式;写锁持有者可以重入写锁,也可以再获取读锁完成锁降级,但已经持有读锁的线程不能直接升级为写锁,否则容易和其他读线程互相等待。它适合读多写少的场景,能提升并发读性能,但要关注非公平模式下写线程饥饿、公平模式下吞吐下降等取舍。

考点 基本语义
主线 三类互斥关系
易错点 只说读写锁能提高性能,却没有说明读读共享、读写互斥、写…

深入解析

01

基本语义

读写锁不是简单的一把互斥锁,而是把临界区访问区分为读操作和写操作。读操作只观察共享数据,不改变状态,因此读读之间可以共享执行;写操作会修改共享数据,必须和任何读操作、任何写操作互斥。实现时通常维护当前读者数量、写者占用状态以及等待队列,保证写锁进入时数据没有并发读写干扰。

02

三类互斥关系

读写锁最重要的判断规则是读读共享、读写互斥、写写互斥。读线程申请锁时,如果没有写锁被持有,一般可以增加读计数并进入;写线程申请锁时,必须确认读计数为零且写计数为零,或者当前线程本身已经持有写锁并进行重入。这个规则决定了它在读多写少场景中比普通互斥锁有更高吞吐。

03

AQS 状态拆分

ReentrantReadWriteLock 使用 AQS 的一个 int 类型 state 同时表示两类计数。通常低 16 位表示写锁的独占重入次数,高 16 位表示读锁的共享持有次数。写锁获取成功时增加低位计数,释放时减少低位计数;读锁获取成功时增加高位计数,释放时减少高位计数。这样避免了两套同步器,也能复用 AQS 的排队、阻塞和唤醒机制。

04

可重入处理

写锁的可重入比较直观:如果当前线程已经是独占锁持有者,再次获取写锁只需要增加写计数。读锁也支持可重入,但由于读锁是共享的,实现中除了总读计数,还需要记录每个线程持有读锁的次数,避免线程释放别人的读锁或释放次数不匹配。ReentrantReadWriteLock 会对首个读线程和线程本地计数做优化,降低常见路径开销。

05

公平与非公平

读写锁通常提供公平和非公平两种策略。公平模式会尊重 AQS 队列顺序,减少写线程长期等待的概率,但会牺牲一部分吞吐;非公平模式允许后来的线程在条件满足时插队获取锁,读多场景下性能更好,但如果读请求持续不断,等待的写线程可能迟迟拿不到锁。实际选择要看系统更重视吞吐还是等待时间上界。

06

锁降级与升级

锁降级是允许的典型流程:线程先持有写锁,完成数据修改后再获取读锁,然后释放写锁,继续以读锁身份读取新状态。这样能保证从写到读的过渡期间状态不被其他写线程插入修改。相反,读锁不能直接升级为写锁,因为多个读线程都尝试升级时,大家都持有读锁并等待其他读者退出,容易形成无法推进的等待。

07

适用边界

读写锁适合共享数据读频率明显高于写频率、读操作耗时足以抵消锁管理成本的场景,例如缓存、配置快照、路由表等。如果写操作频繁,读写互斥会让大量读线程被写线程阻塞,性能可能不如普通互斥锁。StampedLock 可以作为补充提到,它提供乐观读能力,但语义和可重入特性不同,回答读写锁实现时不应偏离主线。

易错点

  • 只说读写锁能提高性能,却没有说明读读共享、读写互斥、写写互斥这三个基本关系。
  • 把 ReentrantReadWriteLock 理解成两把完全独立的锁,忽略它们共享同一个 AQS state 和等待队列。
  • 误认为读锁可以安全升级成写锁,没有意识到多个读线程同时升级可能导致互相等待。
  • 只记住高低位拆分,却说不清读计数和写计数分别代表什么,以及它们如何影响获取条件。
  • 忽略公平和非公平策略的取舍,把非公平模式下可能出现的写线程饥饿问题说成不存在。
  • 认为读写锁适合所有并发场景,没有结合读多写少、临界区成本、写频率这些条件判断收益。

面试官追问

为什么读锁可以共享,而写锁必须独占?

因为多个读操作只读取共享状态,不改变数据,只要没有写线程同时修改,它们之间不会破坏一致性。写操作会改变共享状态,如果和读并发,读线程可能看到中间状态;如果和写并发,多个修改也可能互相覆盖,所以写锁必须独占。

ReentrantReadWriteLock 的 state 是怎么拆分的?

它复用 AQS 的 int 类型 state,通常把低 16 位作为写锁计数,高 16 位作为读锁计数。低位不为零表示存在写锁持有或重入,高位不为零表示存在读锁持有。获取和释放锁时通过位运算更新对应部分。

什么是锁降级,为什么允许它?

锁降级是线程持有写锁时先获取读锁,再释放写锁,从独占访问平滑切换到共享读取。它允许的原因是写锁持有期间没有其他线程能修改数据,先拿到读锁可以保证释放写锁后仍能稳定读取刚更新过的状态。

为什么读锁不能直接升级为写锁?

读锁升级要求当前没有其他读线程存在,但多个读线程可能都持有读锁并等待升级。每个线程都不释放读锁,又都在等待其他读者退出,就会形成无法推进的局面。因此常见实现不支持直接升级。

读写锁一定比 synchronized 或 ReentrantLock 快吗?

不一定。读写锁有更复杂的计数、排队和线程持有记录成本,只有在读多写少且读操作相对有重量时才容易收益。如果写很多、临界区很短或竞争不强,普通互斥锁可能更简单也更快。

StampedLock 和 ReentrantReadWriteLock 有什么关系?

StampedLock 也用于读写并发控制,并额外提供乐观读,适合部分读操作先无锁读取再校验版本的场景。但它不是可重入锁,使用方式和异常释放风险不同,回答读写锁实现时可以简要补充,不应替代主线。