Go语言并发编程面试题精讲(上)

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 为什么需要控制?

  1. 资源限制:内存、端口、文件描述符
  2. 下游服务限制:避免打垮下游服务
  3. 避免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 性能优化原则

  1. 能用原子操作就不用锁
  2. 读多写少用RWMutex
  3. 高并发用分片Map
  4. 控制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并发编程的基础知识:

  1. 原子操作:单变量场景,性能高
  2. 锁机制:多变量场景,保护临界区
  3. 并发安全:Map、Slice的正确使用姿势
  4. 并发控制:限流器、信号量、Channel

核心原则

  • 优先使用原子操作
  • 读多写少用RWMutex
  • 控制goroutine数量
  • 预防并发问题比修复更重要

下一篇文章将深入讲解锁的高级特性、Channel妙用、goroutine管理等进阶内容!


相关阅读


作者 :Go语言面试题整理项目
整理时间 :2026年4月
版权声明:本文为原创文章,转载请注明出处

觉得有用?点个赞👍,关注我学习更多Go语言知识!

相关推荐
不会写DN2 小时前
使用 sync.Once 解决 Go 并发场景下的重复下线广播问题
开发语言·网络·golang
_MyFavorite_2 小时前
JAVA重点基础、进阶知识及易错点总结(36)Lombok 实战 + 阶段总结
java·开发语言
xyq20242 小时前
过滤器模式
开发语言
spencer_tseng2 小时前
AffineTransform cannot be resolved
java
freejackman2 小时前
Java从0到1---基础篇
java·开发语言·后端·idea
CQU_JIAKE2 小时前
4.4【Q】
java·前端·javascript
2301_771717212 小时前
Java自定义注解创建详解
java·开发语言
小陈工2 小时前
Python Web开发入门(十二):使用Flask-RESTful构建API——让后端开发更优雅
开发语言·前端·python·安全·oracle·flask·restful