Go语言并发编程面试题精讲(上):原子操作、锁与并发安全
本文整理了Go语言并发编程的核心知识点,涵盖原子操作、锁机制、并发安全等高频面试题,适合准备面试的同学系统学习。
前言
Go语言的并发编程是其核心特性之一,也是面试中的高频考点。本文整理了第10章前半部分的精华内容,包括原子操作、锁机制、并发安全控制等核心知识点。
一、原子操作与锁的区别
1.1 基本概念
原子操作:在执行过程中不会被其他线程打断的操作,要么全部成功,要么全部失败。
锁:一种同步机制,确保多个goroutine访问共享资源时不会冲突。
1.2 核心区别
| 对比维度 | 原子操作 | 互斥锁 |
|---|---|---|
| 实现方式 | CPU指令级实现 | 软件层面实现 |
| 保护范围 | 单个变量 | 代码片段(临界区) |
| 性能 | 更高(硬件支持) | 相对较低 |
| 使用场景 | 简单计数器、状态变量 | 复杂逻辑、多个变量 |
1.3 选择建议
go
// ✅ 单变量场景:使用原子操作
var counter int64
atomic.AddInt64(&counter, 1)
// ✅ 多变量场景:使用互斥锁
type SafeMap struct {
mu sync.RWMutex
m map[string]int
}
原则:能用原子操作就用原子操作,需要保护多个变量时用锁。
二、运行时资源限制
2.1 限制操作系统线程数量
go
// 限制M(操作系统线程)的最大数量
runtime.SetMaxThreads(500)
// 限制P(逻辑处理器)的数量
runtime.GOMAXPROCS(4)
// 限制单个goroutine的栈大小
debug.SetMaxStack(100000000) // 100MB
// 限制程序总内存
debug.SetMemoryLimit(1 << 30) // 1GB
2.2 实际应用
go
func main() {
// 设置最大线程数为500
oldThreads := runtime.SetMaxThreads(500)
fmt.Printf("旧的最大线程数:%d\n", oldThreads)
// 设置逻辑处理器数量为4
oldProcs := runtime.GOMAXPROCS(4)
fmt.Printf("旧的逻辑处理器数:%d\n", oldProcs)
}
要点:
SetMaxThreads限制M数量,不是goroutine数量GOMAXPROCS影响并发调度效率- IO密集型可设置大于CPU核数
三、Map并发安全解决方案
3.1 为什么Map不安全?
go
// ❌ 并发写会panic
var m = make(map[int]int)
go func() { m[1] = 100 }()
go func() { m[1] = 200 }() // panic: concurrent map writes
3.2 解决方案对比
方案1:RWMutex(推荐)
go
type SafeMap struct {
mu sync.RWMutex
m map[string]int
}
func (sm *SafeMap) Get(key string) int {
sm.mu.RLock() // 读锁
defer sm.mu.RUnlock()
return sm.m[key]
}
func (sm *SafeMap) Set(key string, value int) {
sm.mu.Lock() // 写锁
defer sm.mu.Unlock()
sm.m[key] = value
}
方案2:分片Map(高并发场景)
go
type ShardedMap struct {
shards []*MapShard
count int
}
func (sm *ShardedMap) getShard(key string) *MapShard {
h := fnv.New32a()
h.Write([]byte(key))
return sm.shards[h.Sum32() % uint32(sm.count)]
}
性能对比:
- RWMutex:适合读多写少场景
- 分片Map:适合极高并发场景(分散锁粒度)
- sync.Map:适合读多写少、key稳定场景
四、Slice并发安全问题
4.1 问题演示
go
func TestSliceConcurrencyIssue() {
var slice []int
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
slice = append(slice, n) // ❌ 并发不安全
}(i)
}
wg.Wait()
fmt.Printf("期望: 1000, 实际: %d\n", len(slice))
// 结果:通常小于1000
}
4.2 解决方案
go
type SafeSlice struct {
mu sync.RWMutex
data []int
}
func (ss *SafeSlice) Append(value int) {
ss.mu.Lock()
defer ss.mu.Unlock()
ss.data = append(ss.data, value)
}
func (ss *SafeSlice) Len() int {
ss.mu.RLock()
defer ss.mu.RUnlock()
return len(ss.data)
}
要点:
- Slice并发追加会导致数据丢失
- Map并发写会panic,Slice不会(更隐蔽)
- 使用锁保护或使用Channel串行化
五、原子操作详解
5.1 整数原子操作
go
var x int64
// 存储值
atomic.StoreInt64(&x, 100)
// 增加值
atomic.AddInt64(&x, 1)
atomic.AddInt64(&x, -1) // 传负数表示减
// 读取值
val := atomic.LoadInt64(&x)
// 交换值(返回旧值)
old := atomic.SwapInt64(&x, 200)
// 比较并交换(CAS)
swapped := atomic.CompareAndSwapInt64(&x, 200, 300)
5.2 指针原子操作
go
var ptr unsafe.Pointer
user1 := &User{Name: "Alice", Age: 30}
// 存储指针
atomic.StorePointer(&ptr, unsafe.Pointer(user1))
// 读取指针
user2 := (*User)(atomic.LoadPointer(&ptr))
// CAS操作
user3 := &User{Name: "Bob", Age: 40}
swapped := atomic.CompareAndSwapPointer(
&ptr,
unsafe.Pointer(user1),
unsafe.Pointer(user3)
)
要点:
- 所有操作都是原子的,不会被打断
- 参数必须传指针
- 性能高于锁,但只适合单变量
六、自旋锁实现
6.1 什么是自旋锁?
自旋锁在等待获取锁时不会阻塞,而是在循环中不断尝试(自旋),直到成功。
6.2 实现原理
go
type SpinLock struct {
state int32 // 0=解锁, 1=加锁
}
func (sl *SpinLock) Lock() {
// 不断尝试CAS,直到成功
for !atomic.CompareAndSwapInt32(&sl.state, 0, 1) {
// 自旋等待,不阻塞
}
}
func (sl *SpinLock) Unlock() {
atomic.StoreInt32(&sl.state, 0)
}
6.3 使用场景
go
func TestSpinLock() {
var sl SpinLock
var counter int
for i := 0; i < 1000; i++ {
go func() {
sl.Lock()
counter++
sl.Unlock()
}()
}
}
适用场景:
- ✅ 锁持有时间极短
- ✅ 并发冲突较少
- ❌ 不适合长时间持锁
- ❌ 不适合高竞争场景
七、控制并发数
7.1 为什么需要控制?
- 资源限制:内存、端口、文件描述符
- 下游服务限制:避免打垮下游服务
- 避免goroutine泄露:防止资源耗尽
7.2 实现方式对比
方式1:官方限流器(推荐)
go
import "golang.org/x/time/rate"
// 每秒3个请求,桶容量为3
limiter := rate.NewLimiter(3, 3)
for i := 0; i < 10; i++ {
limiter.Wait(context.Background()) // 等待令牌
go processRequest(i)
}
方式2:信号量
go
import "golang.org/x/sync/semaphore"
// 最多同时3个goroutine
sem := semaphore.NewWeighted(3)
for i := 0; i < 10; i++ {
sem.Acquire(context.Background(), 1)
go func() {
defer sem.Release(1)
processTask(i)
}()
}
方式3:Channel控制
go
slots := make(chan struct{}, 3) // 最多3个并发
for i := 0; i < 10; i++ {
slots <- struct{}{} // 获取槽位
go func() {
defer func() { <-slots }() // 释放槽位
processTask(i)
}()
}
7.3 对比总结
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 官方限流器 | API限流 | 平滑限流,突发支持 | 需要额外依赖 |
| 信号量 | 并发数控制 | 精确控制 | 需要手动释放 |
| Channel | 简单场景 | 无需依赖 | 功能有限 |
八、实战技巧总结
8.1 性能优化原则
- 能用原子操作就不用锁
- 读多写少用RWMutex
- 高并发用分片Map
- 控制goroutine数量
8.2 常见错误
go
// ❌ 错误1:重复关闭channel
close(ch)
close(ch) // panic
// ❌ 错误2:向已关闭channel发送
close(ch)
ch <- 1 // panic
// ❌ 错误3:nil channel读写
var ch chan int
<-ch // 永久阻塞
// ❌ 错误4:Map并发写
var m = make(map[int]int)
// 多个goroutine同时写 -> panic
8.3 最佳实践
go
// ✅ 使用defer确保解锁
mu.Lock()
defer mu.Unlock()
// ✅ 使用context设置超时
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// ✅ 使用WaitGroup等待完成
var wg sync.WaitGroup
wg.Add(n)
// ... goroutine中wg.Done()
wg.Wait()
// ✅ 监控goroutine数量
count := runtime.NumGoroutine()
总结
本文介绍了Go并发编程的基础知识:
- 原子操作:单变量场景,性能高
- 锁机制:多变量场景,保护临界区
- 并发安全:Map、Slice的正确使用姿势
- 并发控制:限流器、信号量、Channel
核心原则:
- 优先使用原子操作
- 读多写少用RWMutex
- 控制goroutine数量
- 预防并发问题比修复更重要
下一篇文章将深入讲解锁的高级特性、Channel妙用、goroutine管理等进阶内容!
相关阅读
作者 :Go语言面试题整理项目
整理时间 :2026年4月
版权声明:本文为原创文章,转载请注明出处
觉得有用?点个赞👍,关注我学习更多Go语言知识!