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语言知识!

相关推荐
逆境不可逃22 分钟前
一篇速通互联网架构的不断升级过程:从单机到云原生
java·elasticsearch·搜索引擎·云原生·架构
代码中介商1 小时前
C++ STL 容器完全指南(二):vector 深入与 stringstream 实战
开发语言·c++
scott.cgi2 小时前
Unity直接编译Java文件作为插件,导致失败的两个打包设置
java·unity·unity调用java·unity的java文件·unity的android插件·unity调用android·unity加载java代码
澈2076 小时前
C++并查集:高效解决连通性问题
java·c++·算法
郝学胜-神的一滴8 小时前
Qt 入门 01-01:从零基础到商业级客户端实战
开发语言·c++·qt·程序人生·软件构建
测试员周周8 小时前
【Appium 系列】第06节-页面对象实现 — LoginPage 实战
开发语言·前端·人工智能·python·功能测试·appium·测试用例
2401_873479408 小时前
运营活动被薅羊毛怎么防?用IP查询+设备指纹联动封堵漏洞
java·网络·tcp/ip·github
ShiJiuD6668889998 小时前
大事件板块一
java
摇滚侠8 小时前
@Autowired 和 @Resource 的区别
java·开发语言
Wy_编程8 小时前
go语言中的结构体
开发语言·后端·golang