深入Mysql锁机制(五)乐观锁CAS

本文探讨了Java中乐观锁的实现机制,重点讲解了Compare and Swap (CAS)技术,对比了悲观锁与乐观锁的区别,分析了CAS的ABA问题及其解决方案。

深入Mysql锁机制(五)乐观锁CAS

线程安全

众所周知,Java是多线程的。但是,Java对多线程的支持其实是一把双刃剑。一旦涉及到多个线程操作共享资源的情况时,处理不好就可能产生线程安全问题。线程安全性可能是非常复杂的,在没有充足的同步的情况下,多个线程中的操作执行顺序是不可预测的。

Java里面进行多线程通信的主要方式就是共享内存的方式,共享内存主要的关注点有两个:可见性和有序性。加上复合操作的原子性,我们可以认为Java的线程安全性问题主要关注点有3个:可见性、有序性和原子性。

Java内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的问题。这里不再详细介绍JMM及锁的其他相关知识。但是我们要讨论一个问题,那就是锁到底是不是有利无弊的?

锁存在的问题

Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有守护变量的锁,都采用独占的方式来访问这些变量。独占锁其实就是一种悲观锁,所以可以说synchronized是悲观锁。

悲观锁机制存在以下问题:

在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

一个线程持有锁会导致其它所有需要此锁的线程挂起。

如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

与锁相比,volatile变量是一个更轻量级的同步机制,因为在使用这些变量时不会发生上下文切换和线程调度等操作,但是volatile不能解决原子性问题,因此当一个变量依赖旧值时就不能使用volatile变量。因此对于同步最终还是要回到锁机制上来。

那么,本文的重点来了,就是深入了解一下乐观锁的实现机制。

乐观锁

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。

上面提到的乐观锁的概念中其实已经阐述了他的具体实现细节:主要就是两个步骤:冲突检测和数据更新。其实现机制就是Compare and Swap(CAS)。

CAS

CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。

这里再强调一下,乐观锁是一种思想。CAS是这种思想的一种实现方式。

Java对CAS的支持

在JDK1.5 中新增java.util.concurrent(J.U.C)就是建立在CAS之上的。相对于对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。

我们以java.util.concurrent中的AtomicInteger为例,看一下在不使用锁的情况下是如何保证线程安全的。主要理解getAndIncrement方法,该方法的作用相当于 ++i 操作。

public class AtomicInteger extends Number implements java.io.Serializable {  

    private volatile int value;  



    public final int get() {  
        return value;  
    }  

    public final int getAndIncrement() {  
        for (;;) {  
            int current = get();  
            int next = current + 1;  
            if (compareAndSet(current, next))  
                return current;  
        }  
    }  

    public final boolean compareAndSet(int expect, int update) {  
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
    }  
}

在没有锁的机制下需要字段value要借助volatile原语,保证线程间的数据是可见的。

这样在获取变量的值的时候才能直接读取。然后来看看++i是怎么做到的。 getAndIncrement采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。而compareAndSet利用JNI来完成CPU指令的操作。

ABA问题

CAS会导致“ABA问题”。

CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

那么,乐观锁是如何解决ABA问题的呢?

部分乐观锁的实现是通过version版本号的方式来解决ABA问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作,否则就执行失败。因为每次操作的版本号都会随之增加,即使本次操作过程中数据被其他线程把数据从A改成B再改回A,那么在写操作进行的时候由于版本号与预期不一致也会执行失败。

<think>好的,用户想了解MySQL中的乐观锁和悲观锁的区别和使用场景。我之前处理过类似的问题,记得有三个引用资料,需要先回顾一下这些内容。首先,引用1提到乐观锁和悲观锁的核心选择标准,包括冲突频率、响应要求和数据敏感度。还有口诀,比如悲观锁像霸道总裁,先占坑再办事,适合高冲突;乐观锁像优雅绅士,先办事再确认,适合低冲突。这应该帮助用户记忆两者的特点。 引用2主要讲悲观锁适合写操作多的场景,因为排它性强,但会长时间限制访问,影响并发性能。而引用3补充了响应速度、冲突频率和重试代价这三个判断因素,比如高响应需求用乐观锁,高冲突用悲观锁,重试代价大用悲观锁。现在需要整理两者的区别和使用场景。区别方面,应该包括实现机制(数据库锁vs版本号/时间戳)、加锁时机(操作前 vs提交时)、性能影响(并发低 vs高)和适用场景。使用场景部分需要分悲观和乐观,结合引用中的例子,比如金融数据用悲观,统计用乐观;高冲突用悲观,低冲突用乐观。 还要注意用户可能的应用场景,比如银行业务,这需要强调悲观锁的重要性。同时,用户可能关心如何选择,所以需要总结口诀和核心标准,方便记忆。 需要确保回答结构清晰,分点说明,并自然融入引用内容。可能还要生成相关问题,比如实现方式、死锁处理、性能优化等,帮助用户深入理解。</think>### MySQL 乐观锁与悲观锁详解 #### 一、核心概念对比 1. **悲观锁** - **实现机制**:通过数据库锁(如`SELECT ... FOR UPDATE`)直接锁定数据行,阻止其他事务操作[^2]。 - **加锁时机**:在事务开始时加锁,操作结束后释放。 - **适用场景**:高并发写操作、金融交易(如银行转账)等需要严格保证数据一致性的场景[^1][^3]。 - **示例**: ```sql BEGIN; SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- 加悲观锁 UPDATE accounts SET balance = balance - 100 WHERE id = 1; COMMIT; ``` 2. **乐观锁** - **实现机制**:通过版本号(`version`)或时间戳(`timestamp`)实现无锁校验,仅在提交时检查数据是否被修改[^1][^3]。 - **加锁时机**:无需显式加锁,提交时通过`WHERE`条件校验。 - **适用场景**:低冲突读多写少场景(如统计报表更新、商品库存秒杀)[^2][^3]。 - **示例**: ```sql UPDATE products SET stock = stock - 1, version = version + 1 WHERE id = 100 AND version = 5; -- 基于版本号的乐观锁 ``` #### 二、核心区别总结 | **维度** | **悲观锁** | **乐观锁** | |----------------|-----------------------------------|-----------------------------------| | **实现方式** | 数据库内置锁机制 | 版本号/CAS机制 | | **并发性能** | 低(长时间锁竞争) | 高(无锁等待) | | **冲突处理** | 直接阻止并发操作 | 提交失败后重试或回滚 | | **适用场景** | 高冲突写操作(如转账) | 低冲突读写操作(如库存扣减) | #### 三、选择标准与口诀 1. **核心选择标准** - **冲突频率**:高冲突用悲观锁(如秒杀系统),低冲突用乐观锁(如评论计数)[^1][^3]。 - **响应要求**:实时性高用悲观锁(如支付系统),允许重试用乐观锁(如数据同步)[^3]。 - **数据敏感度**:金融数据用悲观锁(防篡改),统计数据用乐观锁(允许容错)[^1]。 2. **记忆口诀** > "悲观锁是霸道总裁,先占坑再办事; > 乐观锁是优雅绅士,先办事再确认" [^1]。 #### 四、典型应用场景 1. **悲观锁场景** - 银行转账:事务中锁定账户余额,防止并发操作导致超额扣款[^1]。 - 订单库存预留:通过`FOR UPDATE`锁定库存,确保唯一性[^2]。 2. **乐观锁场景** - 商品库存秒杀:通过版本号控制库存扣减,避免超卖[^3]。 - 多端数据同步:提交时校验版本号,解决数据冲突[^1]。 #### 、性能优化建议 1. 悲观锁优化: ```sql SET GLOBAL innodb_lock_wait_timeout = 30; -- 减少锁等待时间[^1] ``` 2. 乐观锁优化: - 采用指数退避算法(Exponential Backoff)控制重试次数[^1]。 - 避免长事务,减少版本号冲突概率。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值