乐观锁:拿数据的时候都认为在使用该数据的过程中,别人不会修改它,所以在此过程中不会上锁。而当更新数据之后,会判断在此期间有没有其他人更改这个数据。
悲观锁:拿数据的时候都认为在使用过程中,别人会修改它,所以一开始就会上锁,别人想拿该数据就会阻塞,直到获取到锁。(共享资源只给一个线程使用,其他线程阻塞,直到资源使用完后释放锁,其他线程才有权限访问该资源)
乐观锁:适用于多读的类型,可以提高吞吐量。
- CAS机制
- 版本号机制
悲观锁:适用于多写的场景,不会像乐观锁那样出现很多冲突。
- 行锁、表锁、读锁、写锁
- synchronized、ReentrantLock(java)
- sync.Mutex(Golang)
可能出现的问题:
乐观锁:
1、如果在并发量比较高的情况下,如果许多线程反复尝试更新同一共享变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
2、不能保证代码块的原子性。
3、ABA问题。
悲观锁:
1、在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
2、一个线程持有锁会导致其他所有需要此锁的线程阻塞。
3、如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
CAS(Compare and Swap):
- CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
- 更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
ABA问题,就银行存取款场景举例:
1、A在银行有20元,然后A需要取5元,但是实际上A的取款操作提交了两次,开启了两个取款的线程(或goroutine)。
2、如果一个线程成功,一个失败倒没什么关系。但是有可能一个线程成功,一个被阻塞。
3、B往这个账户存了5元。按理讲,这个时候账户应该还是20元。
4、但是之前被阻塞的取款任务重新被运行,看到账户是20元,内存地址值与预期值相同,然后接着做了取款5元的操作。结果账户又变成15元了,B的存款工作白做了。
解决该问题的办法是对每一项操作有一个版本号字段,这样在上面第四步时,看到版本号与自己的不一致,那么就能识别出ABA问题,然后不执行取款操作。
下面是基于Golang语言的乐观锁与悲观锁的测试代码,参考了这篇文章,代码实现简洁易懂,所以我没有做太多改动。
其中使用的sync.Mutex
是悲观锁,使用的atomic
是乐观锁。代码列举了有冲突和无冲突的情况下,乐观锁与悲观锁的执行效率。
在有较小冲突(少量goroutine)的情况下,乐观锁与悲观锁效率相近,当使用多个goroutine的时候,乐观锁的效率明显高于悲观锁(下面的注释代码,有8个goroutine,悲观锁的效率低很多)。同样,当没有冲突的时候,乐观锁的性能依然很好。
import (
"fmt"
"sync"
"time"
"sync/atomic"
)
var wg sync.WaitGroup
var lock sync.Mutex
var times = 10000000
func add(x *int) {
for i := 0; i < times; i++ {
*x++
}
wg.Done()
}
func sub(x *int) {
for i := 0; i < times; i++ {
*x--
}
wg.Done()
}
func addMutex(x *int) {
for i := 0; i < times; i++ {
lock.Lock()
*x++
lock.Unlock()
}
wg.Done()
}
func subMutex(x *int) {
for i := 0; i < times; i++ {
lock.Lock()
*x--
lock.Unlock()
}
wg.Done()
}
func addAtomic(x *int32) {
for i := 0; i < times; i++ {
atomic.AddInt32(x, 1)
}
wg.Done()
}
func subAtomic(x *int32) {
for i := 0; i < times; i++ {
atomic.AddInt32(x, -1)
}
wg.Done()
}
func main() {
var x int = 0
start := time.Now()
wg.Add(2)
go add(&x)
go sub(&x)
wg.Wait()
fmt.Println("No lock: ", x)
elasped := time.Since(start)
fmt.Println(elasped)
start = time.Now()
wg.Add(2)
x = 0
go addMutex(&x)
go subMutex(&x)
wg.Wait()
fmt.Println("Mutex lock with condition race: ", x)
elasped = time.Since(start)
fmt.Println(elasped)
start = time.Now()
wg.Add(2)
var y int32 = 0
go addAtomic(&y)
go subAtomic(&y)
wg.Wait()
fmt.Println("Atomic CAS with condition race: ", y)
elasped = time.Since(start)
fmt.Println(elasped)
//start = time.Now()
//wg.Add(8)
//x = 0
//go addMutex(&x)
//go subMutex(&x)
//go addMutex(&x)
//go subMutex(&x)
//go addMutex(&x)
//go subMutex(&x)
//go addMutex(&x)
//go subMutex(&x)
//wg.Wait()
//fmt.Println("Mutex lock with condition race: ", x)
//elasped = time.Since(start)
//fmt.Println(elasped)
//
//start = time.Now()
//wg.Add(8)
//var y int32 = 0
//go addAtomic(&y)
//go subAtomic(&y)
//go addAtomic(&y)
//go subAtomic(&y)
//go addAtomic(&y)
//go subAtomic(&y)
//go addAtomic(&y)
//go subAtomic(&y)
//wg.Wait()
//fmt.Println("Atomic CAS with condition race: ", y)
//elasped = time.Since(start)
//fmt.Println(elasped)
start = time.Now()
wg.Add(1)
x = 0
go addMutex(&x)
wg.Wait()
fmt.Println("Mutex lock without condition race: ", x)
elasped = time.Since(start)
fmt.Println(elasped)
start = time.Now()
wg.Add(1)
y = 0
go addAtomic(&y)
wg.Wait()
fmt.Println("Atomic CAS without condition race: ", y)
elasped = time.Since(start)
fmt.Println(elasped)
}