面试题答案
一键面试性能差异
- 原子操作:
- 原子操作通常是非常轻量级的,因为它们是由硬件指令直接支持的。比如在x86架构下,有专门的指令来实现原子的读 - 修改 - 写操作。这使得原子操作在执行时开销极小,特别适合于对单个变量的简单操作,如计数器的增减。因为不需要像互斥锁那样进行上下文切换和调度等额外开销,所以在高并发场景下,如果只是对单个变量进行简单的读写或修改操作,原子操作的性能优势明显。
- 互斥锁:
- 互斥锁的开销相对较大。当一个goroutine获取互斥锁时,如果锁已经被其他goroutine持有,那么当前goroutine会被阻塞,操作系统需要进行上下文切换,将其放入等待队列。当锁被释放时,又需要从等待队列中唤醒一个goroutine,这涉及到调度等操作,开销较大。尤其在高并发且锁竞争激烈的场景下,频繁的上下文切换会严重影响性能。
适用场景差异
- 原子操作:
- 适用场景:适用于对简单数据类型(如整数、指针等)的单一操作。例如,在一个高并发的计数器场景中,多个goroutine可能同时对计数器进行加一操作。原子操作可以保证每次加一操作的原子性,避免数据竞争问题。
- 举例:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var counter int64
var wg sync.WaitGroup
numGoroutines := 1000
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&counter, 1)
}()
}
wg.Wait()
fmt.Println("Final counter value:", counter)
}
在这个例子中,使用atomic.AddInt64
函数对counter
进行原子性的加一操作,多个goroutine同时执行也不会出现数据竞争。
2. 互斥锁:
- 适用场景:当需要保护一段复杂的代码逻辑或者多个相关变量时,互斥锁更为合适。例如,在一个银行转账操作中,可能涉及到两个账户余额的修改,这种情况下使用原子操作无法保证整个操作的一致性,而互斥锁可以将涉及到的账户余额修改等操作都保护起来,确保在同一时间只有一个goroutine可以执行转账逻辑,从而保证数据的一致性。
- 举例:
package main
import (
"fmt"
"sync"
)
type BankAccount struct {
balance int
mu sync.Mutex
}
func (a *BankAccount) Withdraw(amount int) {
a.mu.Lock()
defer a.mu.Unlock()
if a.balance >= amount {
a.balance -= amount
}
}
func (a *BankAccount) Deposit(amount int) {
a.mu.Lock()
defer a.mu.Unlock()
a.balance += amount
}
func main() {
account := BankAccount{balance: 1000}
var wg sync.WaitGroup
numGoroutines := 10
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
account.Deposit(100)
account.Withdraw(50)
}()
}
wg.Wait()
fmt.Println("Final account balance:", account.balance)
}
在这个银行账户操作的例子中,使用互斥锁mu
来保护Deposit
和Withdraw
方法中的操作,确保对balance
变量的读写和修改操作在同一时间只有一个goroutine执行,保证了数据的一致性。
总结
- 优先选择原子操作的情况:
- 对简单数据类型(如
int
、int64
、uintptr
等)进行单一的读、写或简单的算术操作,且操作之间相互独立,不需要复杂的逻辑判断和多个变量的协同操作时,优先选择原子操作,以获得更好的性能。
- 对简单数据类型(如
- 优先选择互斥锁的情况:
- 当需要保护一段复杂的代码逻辑,或者涉及多个相关变量的操作,且需要保证操作的一致性和完整性时,优先选择互斥锁。虽然互斥锁性能开销相对较大,但能确保复杂业务逻辑在并发环境下的正确性。