sync.Mutex争用明显时应先确认是否必须使用:对“一次写、多次读”场景,优先考虑sync.RWMutex、sync/atomic或atomic.Value;map高频并发需分片锁;临界区须精简,避免IO操作;RWMutex需防死锁与读饥饿。

sync.Mutex 争用明显时,先确认是不是真该用它
很多锁竞争问题其实源于“默认选了最重的同步方式”。sync.Mutex 是排他锁,读写全阻塞——如果实际场景是配置缓存、状态快照、用户注册表这类“一次写、多次读”,它就是过度设计。
- 用
go tool pprof http://localhost:6060/debug/pprof/mutex看锁等待总时长和争用次数,再结合日志统计真实读写频次;若写操作占比超过 15%~20%,sync.RWMutex反而可能更慢 - 对纯计数、开关标志、指针替换等简单操作,直接上
sync/atomic:比如atomic.AddInt64(&counter, 1)比加锁增减快一个数量级,且无 goroutine 阻塞 - 若数据整体只读、偶有更新(如全局配置结构体),用
atomic.Value存指针,读取零锁:v.Load().(*Config),写入仅一次原子赋值:v.Store(&newCfg)
map 并发读写卡顿?别锁整个 map,按 key 分片
高频并发访问大容量 map 是锁竞争最典型的温床。把所有操作压到一把锁上,等于让所有 goroutine 排队过独木桥。
- 分片数选 32 或 64 较稳妥:太少易形成热点,太多增加内存与哈希开销;避免用
len(key) % N这种弱哈希,改用fnv32或xxhash - 示例中
ShardedMap的Get方法必须确保idx计算无误,且每个shard.mu独立锁定,不能复用同一把锁实例 - 分片锁无法支持跨分片原子操作(比如“求所有 key 的 sum”),这类需求要么退回到全局锁,要么改用
sync.Map+ 外部迭代器(但注意sync.Map不支持遍历保证顺序)
在锁里做 HTTP 请求或 JSON 解析?这是性能雪崩的起点
锁本身不慢,慢的是你让它干了不该干的事。一个 goroutine 持着 sync.Mutex 去调远程 API,其他几十个 goroutine 就全卡在 runtime.futex 上等唤醒。
- 临界区只保留真正需要保护的共享数据操作:比如“查数据库 → 构造新
user结构体 → 加锁写入usersMap”,而不是“加锁 → 查数据库 → 解析 → 写入” - 若必须触发异步动作(如发通知),锁内只做
select发送信号到 channel,由独立 worker 处理 - 对定时刷新类写操作(如 token map 重载),考虑用双缓冲:新数据加载完成后再原子切换指针,避免写过程长期持锁
RWMutex 死锁和饥饿,比性能问题更隐蔽
sync.RWMutex 看似友好,但几个关键约束不守牢,轻则卡死,重则线上静默故障。
立即学习“go语言免费学习笔记(深入)”;
-
RLock()不可嵌套,且绝不能在持有RLock()时调用Lock()——会立即死锁;必须显式RUnlock()后再Lock() - 不要在
defer RUnlock()前做任何可能 panic 或提前 return 的操作,否则RUnlock()被跳过,后续所有写操作永久阻塞 - 写操作频繁时,RWMutex 可能引发读饥饿:大量写请求持续抢占,导致读 goroutine 长时间得不到
RLock();此时应引入写频率控制(如限流、合并批量更新)或改用分片锁











