--- tags: [go, golang, interview, sync-questions] create time: 2026-06-07 14:30 --- # Sync 面试题 🔒 ## 概述 本文件涵盖 Go `sync` 包的 13 道高频面试题,涉及 Mutex、Once、WaitGroup、sync.Map 等同步原语的原理与应用。这些是并发编程中保证数据一致性的核心工具。 ## 关联笔记 - [[hzh/GolangStar/Go语言进阶/Sync]] — Sync 包详细讲解 - [[hzh/GolangStar/Go语言原理/sync.map原理]] — sync.Map 三表结构详解 - [[hzh/GolangStar/Go面试题库/代码面试题]] — 同步场景实战代码 ## 正文 ### Q1:除了 Mutex 还有哪些方式安全读写共享变量? 🟢简单 ## 参考答案 | 方式 | 适用场景 | 性能 | |------|---------|------| | **Channel** | Go 推崇的方式,通过通信传递所有权 | 中 | | **原子操作(atomic)** | 简单的整型/指针操作(计数器、状态位) | 最高 | | **Mutex/RWMutex** | 保护复杂数据结构或多变量一致性 | 中 | | **sync.Map** | 读多写少的并发 Map | 高(特定场景) | > [!tip] 💡 面试技巧 > 选择策略:"简单计数用 atomic,复杂逻辑用 Mutex,读多写少用 sync.Map,跨 goroutine 通信用 Channel。" --- ### Q2-Q3:原子操作 vs 锁的区别? 🟡中等 > [!question] ❓ 思考一下 > 为什么原子操作比锁快?它们各自能保护什么范围的内容? ## 参考答案 | 维度 | 原子操作 | 锁 | |------|---------|---| | 实现层级 | CPU 硬件指令(如 LOCK ADD) | 语言运行时 + 操作系统 | | 保护范围 | 单个变量的单次读写 | 一段代码块(临界区) | | 性能 | 极高,无内核介入 | 较低,失败时 goroutine 挂起 | | 适用场景 | 计数器、标志位 | 多变量一致性、复杂逻辑 | ```go // 原子操作:高性能计数器 var count int64 atomic.AddInt64(&count, 1) // 锁:保护多个变量的一致性 mu.Lock() balance -= amount transactions = append(transactions, tx) mu.Unlock() ``` > [!note] 📝 核心考点 > 原子操作是"微观"的,锁是"宏观"的。原子操作不挂起 goroutine,锁失败时会 park。 --- ### Q4:Mutex 底层怎么实现的? 🟡中等 ## 参考答案 Mutex 通过**原子操作 + 信号量**实现: ```go type Mutex struct { state int32 // 锁的状态(锁定/唤醒/饥饿模式等,用二进制位标识) sema uint32 // 信号量,用于 goroutine 的阻塞和唤醒 } ``` > [!info] 🔗 延伸阅读 > - [[hzh/GolangStar/Go语言原理/gmp调度原理]] — GMP 与 Mutex 的关系 --- ### Q5:Mutex 有几种模式? 🟡中等 ## 参考答案 两种模式,动态切换: | 模式 | 特点 | 公平性 | |------|------|--------| | **正常模式** | 新来的 goroutine 可以和队列头竞争,吞吐量高 | 不公平 | | **饥饿模式** | 等待超过 1ms 后进入,锁直接交给队头,新来者排尾部 | 绝对公平 | **切换规则:** - 进入饥饿:等待时间 > 1ms - 退出饥饿:等待队列空 或 等待时间 < 1ms > [!tip] 💡 面试技巧 > "Go 在性能和公平之间做了精妙的平衡:正常情况下追求吞吐,发现有人被饿死时就切换到公平模式。" --- ### Q6:自旋的 goroutine 会占用太多资源吗? 🟡中等 ## 参考答案 **不会。** Go 的自旋设计非常克制: 1. 只在特定条件下触发(CPU 核数 > 1 且排队 goroutine 不多) 2. 持续时间极短(几十纳秒) 3. 目的是避免更昂贵的上下文切换开销 > [!warning] ⚠️ 高频陷阱 > 自旋是"机会主义的短线优化"——赌锁马上就释放了,与其花大代价挂起+唤醒 goroutine,不如原地稍等一下。 --- ### Q7:锁释放后哪个 goroutine 优先获取? 🟡中等 ## 参考答案 取决于当前模式: | 模式 | 谁先拿到锁 | |------|-----------| | 正常模式 | 队头 goroutine **和新来的自旋 goroutine 竞争**,新来的可能插队 | | 饥饿模式 | **队头 goroutine 直接获得**,新来者排尾部 | --- ### Q8:sync.Once 的作用和底层原理? 🟡中等 > [!question] ❓ 思考一下 > 如果两个 goroutine 同时调用 Once.Do(f),f 会被执行几次?如何实现这个保证? ## 参考答案 **作用**:确保一个函数在整个程序生命周期内只执行一次,常用于单例初始化。 ```go type Once struct { done uint32 // 标识位:0=未执行,1=已执行 m Mutex } ``` **执行流程:** ```mermaid graph TD A[Once.Do f] --> B{atomic.Load done == 1?} B -->|是| C[直接返回, 无锁] B -->|否| D[加锁 doSlow] D --> E{再次检查 done == 1?} E -->|是| F[解锁, 返回] E -->|否| G[执行 f] G --> H[atomic.Store done = 1] H --> I[解锁] ``` > [!note] 📝 核心考点 > **双重检查(Double-Checked Locking)**是关键:第一次用原子操作快速判断(无锁路径),第二次加锁后再判断防止重复执行。 --- ### Q9:WaitGroup 怎样实现协程等待? 🟡中等 ## 参考答案 WaitGroup 本质是**原子计数器 + 信号量的协作**: ```go type WaitGroup struct { noCopy noCopy // vet 静态检查防复制 state atomic.Uint64 // 64位:高32位=计数器, 低32位=等待者数量 sema uint32 // 信号量 } ``` **工作流程:** 1. `Add(n)`:计数器 += n 2. `Done()`:计数器 -= 1 3. `Wait()`:计数器 != 0 则通过信号量挂起当前 goroutine 4. 最后一个 `Done()` 将计数器归零 → 信号量一次性唤醒所有等待者 > [!warning] ⚠️ 高频陷阱 > WaitGroup **不能被复制**!一旦使用过就不能拷贝,否则会导致状态不一致。`noCopy` 字段让 `go vet` 能在编译期检测这个问题。 --- ### Q10:sync.Map 的底层原理? 🟡中等 ## 参考答案 核心思想:**空间换时间**,通过 read/dirty 双 map 实现读操作的无锁优化。 ```go type Map struct { mu Mutex // 保护 dirty read atomic.Value // 实际存储 readOnly dirty map[interface{}]*entry // 需要加锁访问 misses int // read 未命中次数 } type readOnly struct { m map[interface{}]*entry amended bool // true 表示 dirty 中有 read 没有的数据 } type entry struct { p unsafe.Pointer // 指向真正的 value } ``` > [!info] 🔗 延伸阅读 > - [[hzh/GolangStar/Go语言原理/sync.map原理]] — 完整的三表结构图解 --- ### Q11:read map 和 dirty map 的关联? 🟢简单 ## 参考答案 | Map | 角色 | 特性 | |-----|------|------| | read | 只读缓存 | 无锁读取,可能是过期快照 | | dirty | 最新全集 | 需加锁,包含所有最新数据 | 当 `misses == len(dirty)` 时,dirty 晋升为新的 read。 --- ### Q12:为什么设计 nil 和 expunged 两种删除状态? 🟡中等 ## 参考答案 为了解决 **"如何在只读的 read map 上高效删除"** 的问题: - **expunged**:逻辑删除标记。key 只存在于 read 中时,不能物理删除,标记为 expunged,读操作看到它就直接返回 `nil, false` - **nil**:中间状态,用于 dirty 和 read 同步过程中表示 key 正在被删除或迁移 > [!tip] 💡 面试技巧 > "这是典型的用状态标记换取无锁性能的设计。一次 Delete 不需要加锁复制整个 read map,而是打一个标记,延迟到 dirty 晋升时再做物理删除。" --- ### Q13:sync.Map 适用的场景? 🟢简单 ## 参考答案 **读多写少**的场景。期望流量在 read map 层被拦截,避免频繁加锁访问 dirty map。 > [!warning] ⚠️ 高频陷阱 > 如果写操作过多,sync.Map 基本等价于一把互斥锁 + map,效率反而比普通 map + Mutex 更低。 ## 关联笔记 - [[hzh/GolangStar/Go语言进阶/Sync]] - [[hzh/GolangStar/Go语言原理/sync.map原理]] - [[hzh/GolangStar/Go面试题库/代码面试题]]