面试题答案
一键面试设计场景
假设我们有一个银行账户结构体,包含账户余额,并提供一个存款方法 Deposit
,在高并发环境下多个协程可能同时调用该方法向账户存钱,需要保证余额数据的一致性和安全性。
package main
import (
"fmt"
"sync"
)
// BankAccount 银行账户结构体
type BankAccount struct {
balance int
mu sync.Mutex
}
// Deposit 存款方法
func (b *BankAccount) Deposit(amount int) {
b.mu.Lock()
defer b.mu.Unlock()
b.balance += amount
}
// GetBalance 获取余额方法
func (b *BankAccount) GetBalance() int {
b.mu.Lock()
defer b.mu.Unlock()
return b.balance
}
使用示例
func main() {
account := BankAccount{}
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
account.Deposit(100)
}()
}
wg.Wait()
fmt.Println("Final Balance:", account.GetBalance())
}
使用互斥锁实现并发安全
- 方法定义:在
BankAccount
结构体中定义一个sync.Mutex
类型的字段mu
。在Deposit
方法中,通过调用mu.Lock()
来锁定互斥锁,保证同一时间只有一个协程可以进入临界区(修改余额的代码部分),操作完成后通过defer mu.Unlock()
解锁。 - 锁机制:这里使用互斥锁
sync.Mutex
,它可以保证在同一时刻只有一个协程能够访问共享资源(账户余额),从而避免数据竞争,保证数据一致性和安全性。
使用读写锁优化读操作(如果有读多写少场景)
如果存在大量读取余额操作和少量存款操作,可以使用读写锁 sync.RWMutex
。
// BankAccount 银行账户结构体
type BankAccount struct {
balance int
mu sync.RWMutex
}
// Deposit 存款方法
func (b *BankAccount) Deposit(amount int) {
b.mu.Lock()
defer b.mu.Unlock()
b.balance += amount
}
// GetBalance 获取余额方法
func (b *BankAccount) GetBalance() int {
b.mu.RLock()
defer b.mu.RUnlock()
return b.balance
}
使用通道实现并发安全(另一种思路)
package main
import (
"fmt"
"sync"
)
// BankAccount 银行账户结构体
type BankAccount struct {
balance int
ch chan int
}
// NewBankAccount 创建新的银行账户
func NewBankAccount() *BankAccount {
b := &BankAccount{
ch: make(chan int),
}
go func() {
for {
amount := <-b.ch
b.balance += amount
}
}()
return b
}
// Deposit 存款方法
func (b *BankAccount) Deposit(amount int) {
b.ch <- amount
}
// GetBalance 获取余额方法
func (b *BankAccount) GetBalance() int {
b.ch <- 0
return b.balance
}
性能和资源利用优缺点分析
- 互斥锁
- 优点:实现简单直观,对于读写操作都能有效保证数据一致性,适用于读写操作频率相近的场景。
- 缺点:在高并发写操作时,由于同一时间只有一个协程能获取锁,可能会导致性能瓶颈,因为其他协程需要等待锁释放。而且如果锁使用不当,可能会造成死锁。
- 读写锁
- 优点:在读多写少的场景下性能较好,允许多个协程同时读数据,因为读操作不会修改数据,所以不会产生数据竞争。写操作时会独占锁,保证数据一致性。
- 缺点:实现相对复杂,需要区分读锁和写锁的获取与释放。并且如果写操作频繁,读操作也可能会因为等待写锁而出现性能问题。
- 通道
- 优点:通过将数据操作串行化,避免了锁的竞争,理论上在高并发场景下有更好的性能表现,尤其是对于大量并发操作的场景。同时,通道的使用符合Go语言的CSP模型,使得代码结构更清晰。
- 缺点:实现逻辑相对复杂,需要通过通道传递数据和控制逻辑,对于简单场景可能会过度设计。而且通道本身也会占用一定的资源,如果通道管理不当,可能会导致死锁或资源泄漏。