登录
首页 >  Golang >  Go教程

Sync.Map读写原理全解析

时间:2026-05-09 11:28:56 468浏览 收藏

sync.Map 并非万能的并发安全映射,其精巧的 read/dirty 双层结构与原子操作仅在“读多写少、键生命周期长、极少遍历”的特定场景下才能发挥优势:read 依赖 atomic.Pointer 实现真正无锁读取,dirty 则通过 misses 计数器动态升级以平衡新鲜度与拷贝开销;Store 优先无锁更新,失败后才加锁处理,Delete 仅为逻辑标记,真实清理延迟至 dirty 提升时完成——但一旦写入频繁、删除密集或需定期 Range,它反而比普通 sync.RWMutex + map 更慢、更耗内存,用错场景得不偿失。

Go 语言中 sync.Map 的 read 与 dirty 机制详解

sync.Map 不是通用并发 map,它的 read/dirty 分离只在“读多写少”且键生命周期长的场景下才真正高效;用错场景反而比加锁的 map 更慢、更占内存。

read 为什么能无锁读?靠的是 atomic.Pointer + readOnly 结构体

read 字段不是普通 map,而是一个 atomic.Pointer,指向一个 readOnly 结构体。这个结构体里包含两个东西:m(底层是 map[interface{}]*entry)和 amended(布尔值)。atomic.Pointer 保证了对整个 readOnly 的替换是原子的,所以读操作只需一次原子加载,拿到指针后直接查 m,全程不碰锁。

但要注意:entry.punsafe.Pointer,读取时需用 atomic.LoadPointer 判断状态——nil 表示已删,expunged 表示该 entry 已从 dirty 中物理移除,只有其他有效指针才代表可用值。

dirty 什么时候被提升为 read?由 misses 计数器触发

每次 Loadread.m 中没找到、转去查 dirty 时,misses 就加 1。当 misses >= len(dirty),就触发提升:把整个 dirty 赋给新的 readOnly.m,清空 dirty,重置 misses 为 0,并将 read.amended 设为 false

这个阈值设计很关键:

  • 太小 → 频繁提升,拷贝开销大,dirty 刚写几条就升,失去写缓冲意义
  • 太大 → read 长期 stale,大量读被迫进锁查 dirty,并发读性能崩塌
  • Go 当前实现就是用 len(dirty) 作阈值,不暴露可调接口,你没法改

Store 操作为什么有时无锁、有时要锁?取决于 entry 是否存在且未被 expunged

Store 第一步永远是查 read.m

  • 如果 key 存在且 entry.p != expunged → 直接 atomic.StorePointer 更新值,无锁完成
  • 如果 key 不存在,或 entry.p == expunged → 必须加 mu 锁,进入 dirty 分支处理

加锁后还要双检 read(防止别的 goroutine 刚升完级),再决定是往 dirty 插新 entry,还是把 expunged 条目复活。这个“先试无锁、失败再锁”的路径,正是它读多时快的核心。

Delete 是假删除,真清理要等 dirty 提升

Delete 永远只动 read.m 里的 entry.p,把它设成 nil(标记逻辑删除),不会碰 dirty。只有当该 key 同时存在于 dirty 中时,才会在加锁后从 dirty 中真正删掉。

但更常见的情况是:key 只在 read 里,Delete 后它就一直挂着 nil,直到某次 dirty 提升为新 read 时,这些 nil entry 才被彻底过滤掉。这意味着:

  • 内存不会立刻释放,可能累积 stale 条目
  • Range 遍历时会跳过 nilexpunged,但遍历本身会强制触发一次提升,代价不小
  • 别指望 Delete 后立即减小内存占用

真正容易被忽略的是:sync.Map 的“高性能”完全绑定在 key 稳定、写入频次低、且几乎不遍历的前提下。一旦开始高频写、反复删、或者定期 Range,它的优势就迅速瓦解,甚至不如自己包一层 sync.RWMutex + map

终于介绍完啦!小伙伴们,这篇关于《Sync.Map读写原理全解析》的介绍应该让你收获多多了吧!欢迎大家收藏或分享给更多需要学习的朋友吧~golang学习网公众号也会发布Golang相关知识,快来关注吧!

资料下载
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>