三条门规:读读放行,谁写谁独占写完的数据,读的人必须看得见Java 的账本:一个 long 掰成两半Go 的账本:一个会变负的计数器Python 标准库为啥至今没有读写锁掏出共同套路,再泼一盆冷水

先设想一个特别常见的场景:你有一个全局配置对象,服务一启动,几百个线程都在读它,偶尔有个管理接口进来改一次。

图省事,直接一把互斥锁(Mutex)锁住整个对象。问题跟着来了——两个线程同时读,明明互不干扰,也得一个先进、另一个在门口干等。读的人越多,队排得越长。

谁都没改数据,凭什么要排队?读写锁(ReadWriteLock)就是冲着这笔冤枉账来的。今天把它的门规、底层记账方式,还有它什么时候反而不划算,一次聊透。

读写锁把进门的人分成两种——读者和写者,然后立下三条规矩:

  • •读 + 读:放行。多个读者同时进,互不影响
  • •读 + 写:互斥。写者进场,读者统统靠边
  • •写 + 写:互斥。写者之间照旧排队

写这边的代码和普通锁几乎一个模样,拿经典的 counter++ 举例(Java):

打开网易新闻 查看精彩图片

注意,counter++ 之所以没乱,不是这条语句突然变成了原子指令,而是写锁把冲突的临界区串行化了——大家排着队一个个改,自然改不坏。

把它和普通互斥锁摆在一起看,差别就一处:

打开网易新闻 查看精彩图片

所以读写锁并不是一把「更强的锁」,它只是把原来唯一的入口拆成两个:读者走共享入口,写者走独占入口,只放开了「读 + 读」这一个口子。

光会放行还不够。读写锁还藏着一条更隐蔽的保证:写者释放写锁之后拿到读锁的人,必须能看到写者写下的所有内容,顺序还不能乱。

还是拿 counter 和 ready 这对老搭档演示:

打开网易新闻 查看精彩图片

要是没有这条保证,线程 B 完全可能读到 ready 为 true、counter 却还是 0 的精神分裂现场。这条保证在底层是一条严格的先后链条:

打开网易新闻 查看精彩图片

写锁释放到读锁获取之间,有一堵「内存同步」的墙。正是这堵墙,把写者落盘的所有改动,原封不动地推给了后来的读者。

规矩讲完了,底层怎么记账?以 JDK 为例,ReentrantReadWriteLock 底层站在同步框架 AQLS 上,核心状态就是一个 64 位的 state 变量,被掰成了两半用:

打开网易新闻 查看精彩图片

读者进来,用 CAS 给高位加一,多人同时成功毫无压力;写者进来,要求整个 state 都没有冲突才放行。而 state 本身被声明为 volatile——第 2 节那条「写完必须被看见」,不是什么魔法,就是靠写锁释放时的 volatile 写、读锁获取时的 volatile 读/CAS这条路径撑起来的。

抢不到锁的人也不空转:进 AQS 的等待队列 park 睡觉,等持锁人释放时被 unpark 叫醒,重新参与竞争。省 CPU,也公平。

Go 的 sync.RWMutex 管的是同一套门规,记账方式却完全是另一路:不是一个变量掰两半,而是内部 Mutex + 原子计数器 + 两个信号量的组合拳。用法长这样:

打开网易新闻 查看精彩图片

精髓全在 readerCount 这个计数器上,它靠正负号传递信息:

打开网易新闻 查看精彩图片

两种实现看着像双胞胎,脾气差异其实不小:

打开网易新闻 查看精彩图片

还有一点两者完全一致:底层都没有发明新的硬件原语,全靠原子指令、缓存一致性、内存序这三样老底子撑着。区别只在软件层怎么组合它们——Java Park 的是线程,Go Park 的是协程,调度器还能腾出手去跑别人。

有意思的是,Python 的 threading 里到今天都没有读写锁。不是做不出来——2009 年就有人提了提案(Issue 8800),讨论了十几年也没进标准库。

原因有两个。第一是GIL 把收益砍没了:传统 CPython 同一时刻只有一个线程在跑 Python 代码,你就算让一百个读者同时进场,它们也只能轮流转,放行了个寂寞(没有 GIL 的 free-threaded 版本正在改变这一点)。

第二是吵不拢:API 该长什么样(一个统一的 RWLock,还是两个独立的锁)?写者排队时新读者放不放行?能不能升级降级?这些语义一旦进标准库就得永久兼容,而社区始终没拿出统一方案。

真要用,一般两条路:找个第三方库,或者用 Lock 加 Condition 自己攒一个——当年那个提案本身就是这么实现的。

把 Java 和 Go 的实现皮扒掉,剩下的骨架是同一套三步:

  • •原子记账:随时维护「几个读者在读、写者在不在」的状态,且每次改动必须原子
  • •冲突睡觉:抢不到就进队列 park,等持锁人释放后唤醒重试,不空转烧 CPU
  • •同步边界:「写锁释放 → 读锁获取」这条边界负责内存同步,让后到的读者看见前面所有写入

但先别急着把项目里的 Mutex 全换成读写锁——它记账是有成本的。读者数量本身就是个共享状态,每个读者进出都要原子加减一次,读的人一多,这个计数器反而成了新的堵点。

打开网易新闻 查看精彩图片

一句大实话:读写锁不是「升级版 Mutex」,它是一笔针对特定场景的交换——用更复杂的记账,换读并发。场景不对,这笔买卖就是亏的。

你的项目里有没有那种「明明全是读,却一把大锁堵到底」的地方?评论区聊聊,看看多少人交过这个学费