sync.Mutex 不支持重入,因未记录持有者 goroutine ID,重复 Lock() 会导致永久阻塞;自旋仅在多核、M 未被抢占、锁持有者正运行且尝试次数<4 时发生;RWMutex 读并发高但写需等所有读者退出,仅适用于读远多于写的轻量写场景;channel 内部亦使用 Mutex 保护。

sync.Mutex 为什么不能重入
Go 的 sync.Mutex 不支持重入,不是疏漏,而是设计取舍。它内部没有记录持有锁的 goroutine ID(GID),Lock() 只检查 state 字段的 mutexLocked 位,一旦为 1 就进入等待队列或自旋;再次调用 Lock() 时仍会尝试获取——但锁已被自己持有着,结果就是永久阻塞。
常见错误现象:fatal error: all goroutines are asleep - deadlock,或者更早触发 panic: sync: unlock of unlocked mutex(如果误在未 Lock 时 Unlock)。
- 不要在同一个 goroutine 中多次
Lock(),哪怕中间嵌套了函数调用 - 避免在 defer 中无条件
Unlock()后又手动Unlock(),这是典型重入误用场景 - 若需重入语义(如递归调用临界区),应改用
sync.RWMutex+ 读锁组合,或自行封装带 goroutine ID 检查的锁(不推荐)
Mutex 自旋发生的实际条件
sync.Mutex 的自旋不是“一直忙等”,而是在特定上下文下、有限次数内的试探性等待。它的触发依赖 runtime 层的 canSpin(i) 判断,核心条件包括:
- 当前 goroutine 所绑定的 M(系统线程)未被抢占,且处于可运行状态
- 尝试获取锁的轮次
i小于 4(Go 1.22+ 源码中固定为active_spin = 4) - 锁的持有者 goroutine 正在运行(
gp.preempt == false),大概率很快释放 - CPU 核心数 > 1(单核自旋毫无意义,只会饿死其他 goroutine)
一旦不满足任一条件,立即退回到 futex 阻塞路径。这意味着:高负载、低核数、或锁持有时间长(>1ms)的场景下,自旋基本不会发生——别指望靠“短锁+自旋”掩盖设计缺陷。
立即学习“go语言免费学习笔记(深入)”;
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
sync.RWMutex 在读多写少场景下的真实开销
sync.RWMutex 的读并发能力是实打实的,但代价藏在写操作上。它的结构体里有 wrterSem 和 readerSem 两个信号量,以及一个写锁标志位。关键点在于:
- 每次
RLock()都要原子增减rCount,并检查是否已有写者在等 —— 这不是零成本 -
Lock()会先阻塞新读者(通过置位writersWait),再等待所有活跃读者退出,最后才真正获得写权 - 当写竞争激烈时,
RWMutex的写延迟可能比Mutex更高,因为要等“全体读者撤离”
所以它只在读操作远多于写操作(比如读:写 ≥ 10:1)、且写操作本身极轻量(如更新一个字段)时才明显优于 Mutex。拿它保护高频更新的 map,反而容易拖慢整体吞吐。
channel 内部其实也用 Mutex
很多人以为 channel 是“无锁”的高级抽象,其实不然。翻看 runtime/chan.go,hchan 结构体末尾明明白白写着 lock mutex。所有发送(ch )、接收(<code><-ch)、关闭(close(ch))操作,第一步都是调用 lock(&c.lock)。
这意味着:
- channel 的线程安全,本质是靠底层
mutex串行化访问,不是魔法 - 高并发下大量 goroutine 抢同一个 channel,锁争用一样会发生,性能瓶颈和
sync.Mutex场景高度相似 - 别为了“看起来更 Go 风格”而强行用 channel 替代锁——比如用带缓冲 channel 做计数器,不如直接用
atomic.AddInt64
真正该警惕的,不是“用了锁”,而是没想清楚锁的粒度、持有时间、和竞争模式。

















