乐观锁、悲观锁以及CAS机制的研究

本文深入探讨了乐观锁和悲观锁的概念及其适用场景,指出乐观锁适合多读操作,而悲观锁适用于多写场景。同时,文章详细介绍了CAS(Compare and Swap)机制,并分析了其在高并发下的ABA问题。通过Golang代码示例,展示了乐观锁和悲观锁在有冲突和无冲突情况下的执行效率,结果显示,在多goroutine环境下,乐观锁的性能更优。

乐观锁:拿数据的时候都认为在使用该数据的过程中,别人不会修改它,所以在此过程中不会上锁。而当更新数据之后,会判断在此期间有没有其他人更改这个数据。
悲观锁:拿数据的时候都认为在使用过程中,别人会修改它,所以一开始就会上锁,别人想拿该数据就会阻塞,直到获取到锁。(共享资源只给一个线程使用,其他线程阻塞,直到资源使用完后释放锁,其他线程才有权限访问该资源)

乐观锁:适用于多读的类型,可以提高吞吐量。

  • 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)
}
### 乐观锁悲观锁CAS实现机制 #### 悲观锁 悲观锁的核心理念在于认为并发环境下的资源访问一定会发生冲突,因此在操作共享资源之前会先对其进行锁定。这种锁通常由数据库或者程序中的显式锁来实现[^2]。例如,在SQL语句中可以通过`SELECT ... FOR UPDATE`的方式对记录加锁,从而防止其他事务修改该记录[^5]。 ```sql -- 使用悲观锁的 SQL 示例 SELECT status FROM t_goods WHERE id=1 FOR UPDATE; ``` 悲观锁的主要特点是它能够完全阻止其他线程或进程在同一时间访问相同的资源,适用于写密集型场景,但在高并发读取的情况下可能会带来性能瓶颈。 --- #### 乐观锁 乐观锁则基于一种假设:大多数情况下不会存在并发冲突,所以在执行更新前不需要加锁。当检测到冲突时,则采取重试策略直至成功[^3]。其主要实现方式有两种: - **版本号机制** 数据表中增加一个字段用于表示当前数据的状态(通常是整数类型的版本号)。每次更新时都会验证此版本号是否匹配最新状态;如果不匹配说明有其他事务已经对该条目进行了更改,此时可以抛出异常或者重新尝试提交新请求[^4]。 - **CAS (Compare And Swap)** CAS 是一种无锁算法,通过比较内存位置上的旧值与预期值是否相同来进行原子化交换操作。只有当两者相等时才会替换为目标值并返回 true 表明此次变更有效;否则仅反馈 false 而不做任何改动[^1]。 以下是利用 `java.util.concurrent.atomic.AtomicInteger` 类演示如何运用 CAS 方法的一个简单例子: ```java import java.util.concurrent.atomic.AtomicInteger; public class CasExample { private static AtomicInteger atomicInt = new AtomicInteger(0); public void increment() { int expectValue, newValue; do { expectValue = atomicInt.get(); // 获取当前值 newValue = expectValue + 1; // 计算新的期望值 } while (!atomicInt.compareAndSet(expectValue, newValue)); // 尝试设置新值 } } ``` 上述代码片段展示了如何借助于 compare-and-swap 来保证计数值的安全递增过程即使面对多个线程竞争也依然保持一致性。 --- #### 区别对比 | 特性 | 悲观锁 | 乐观锁 | |--------------------|---------------------------------|--------------------------------| | 基本思想 | 预期会有冲突 | 预期不会有冲突 | | 锁定时机 | 提前锁定 | 更新时刻才判断 | | 并发处理能力 | 较低 | 更高效 | | 性能影响 | 可能导致死锁等问题 | 失败需重试 | | 应用场景 | 写频繁的数据集 | 主要是读多写的业务逻辑 | 尽管这两种锁各有优劣,但实际应用过程中往往需要根据具体需求权衡选择合适的方案。对于那些经常面临大量随机存取压力的应用来说,采用乐观控制可能更为合适一些;而对于某些特定领域比如银行转账之类涉及金钱转移的操作而言,由于安全性至关重要所以更倾向于依赖传统的互斥手段即悲观封锁形式加以保护。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值