CAS(乐观锁)以及ABA问题

独占锁是一种悲观锁,synchronized就是一种独占锁;它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起直到持有锁的线程释放锁。

所谓乐观锁就是每次不加锁,假设没有冲突而去完成某项操作;如果发生冲突了那就去重试,直到成功为止。

CAS(Compare And Swap)是一种有名的无锁算法。CAS算法是乐观锁的一种实现。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B并返回true,否则返回false。

注:synchronized和ReentrantLock都是悲观锁。

注:什么时候使用悲观锁效率更高、什么使用使用乐观锁效率更高,要根据实际情况来判断选择。

什么是CAS机制

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

这样说或许有些抽象,我们来看一个例子:

1.在内存地址V当中,存储着值为10的变量。

2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。

3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。

5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。

7.线程1进行SWAP,把地址V的值替换为B,也就是12。

从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

CAS的优缺点:

乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,乐观锁是对悲观锁的改进,虽然它也有缺点,但它确实已经成为提高并发性能的主要手段,而且jdk中的并发包也大量使用基于CAS的乐观锁。但它也有缺点,如下:

1.CPU可能开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用悲观锁了。

3.ABA问题。
CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。

ABA问题:

线程1准备用CAS修改变量值A,在此之前,其它线程将变量的值由A替换为B,又由B替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了。

 

ABA问题处理:

思路:解决ABA最简单的方案就是给值加一个修改版本号,每次值变化,都会修改它版本号,CAS操作时都对比此版本号。

JAVA中ABA中解决方案(AtomicStampedReference/AtomicMarkableReference)

AtomicStampedReference 本质是有一个int 值作为版本号,每次更改前先取到这个int值的版本号,等到修改的时候,比较当前版本号与当前线程持有的版本号是否一致,如果一致,则进行修改,并将版本号+1(当然加多少或减多少都是可以自己定义的),在zookeeper中保持数据的一致性也是用的这种方式;

AtomicMarkableReference则是将一个boolean值作是否有更改的标记,本质就是它的版本号只有两个,true和false,修改的时候在这两个版本号之间来回切换,这样做并不能解决ABA的问题,只是会降低ABA问题发生的几率而已;

例子:

private static AtomicStampedReference<Integer> atomicStampedRef =
        new AtomicStampedReference<>(1, 0);
public static void main(String[] args){
    Thread main = new Thread(() -> {
        System.out.println("操作线程" + Thread.currentThread() +",初始值 a = " + atomicStampedRef.getReference());
        int stamp = atomicStampedRef.getStamp(); //获取当前标识别
        try {
            Thread.sleep(1000); //等待1秒 ,以便让干扰线程执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        boolean isCASSuccess = atomicStampedRef.compareAndSet(1,2,stamp,stamp +1);  //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
        System.out.println("操作线程" + Thread.currentThread() +",CAS操作结果: " + isCASSuccess);
    },"主操作线程");

    Thread other = new Thread(() -> {
        Thread.yield(); // 确保thread-main 优先执行
atomicStampedRef.compareAndSet(1,2,atomicStampedRef.getStamp(),atomicStampedRef.getStamp() +1);
        System.out.println("操作线程" + Thread.currentThread() +",【increment】 ,值 = "+ atomicStampedRef.getReference());
        atomicStampedRef.compareAndSet(2,1,atomicStampedRef.getStamp(),atomicStampedRef.getStamp() +1);
        System.out.println("操作线程" + Thread.currentThread() +",【decrement】 ,值 = "+ atomicStampedRef.getReference());
    },"干扰线程");

    main.start();
    other.start();
}

文章参考:https://www.jianshu.com/p/ae25eb3cfb5d

https://blog.csdn.net/justry_deng/article/details/83449038

<think>好的,用户想了解CAS乐观锁在Java并发中的工作原理和实现方式。首先,我需要回忆一下CAS的基本概念。CAS全称是Compare-And-Swap,是一种无锁算法,用于实现多线程环境下的同步操作。它的核心思想是先比较内存中的值是否与预期值相同,如果相同则更新为新值,否则不进行操作。 接下来,用户提到了Java中的实现,特别是Unsafe类和AtomicInteger。记得Unsafe类提供了硬件级别的原子操作,但一般不建议直接使用,因为它不安全。而AtomicInteger等原子类则是通过CAS来实现的,比如incrementAndGet方法。需要解释CAS的三个操作数:内存地址、预期值和新值。当执行CAS时,处理器会保证这个操作的原子性。 然后,要考虑到CAS可能存在的问题,比如ABA问题。这时候需要提到AtomicStampedReference或AtomicMarkableReference,它们通过引入版本号或标记来解决ABA问题。另外,自旋时间过长也是问题,可能导致CPU资源浪费,这时候可以结合适应性自旋或其他锁机制来优化。 用户提供的引用资料里提到了乐观锁和悲观锁的区别,这里需要对比一下。悲观锁认为并发冲突概率高,所以先加锁再操作,比如synchronized;而乐观锁假设冲突少,只在提交时检查,适用于读多写少的场景。 在Java中,CAS的实现依赖于Unsafe类的compareAndSwap方法,但具体到原子类如AtomicInteger,其内部实现会调用Unsafe的方法。例如,incrementAndGet()方法内部使用了一个循环,不断尝试CAS操作直到成功。 还要注意CAS的优缺点。优点是减少线程阻塞,提高并发性能;缺点则是ABA问题和自旋开销。需要举例说明,比如AtomicStampedReference的使用方法,如何通过版本号避免ABA问题。 最后,用户可能需要相关的问题来进一步学习,比如CAS在Java中的应用场景、ABA问题的具体案例、与synchronized的对比等。这些相关问题可以帮助用户深入理解CAS机制。 总结回答的结构应该包括CAS的定义、工作原理、Java中的实现(原子类)、优缺点、问题及解决方案,以及相关问题的推荐。需要确保使用正确的中文术语,并正确引用提供的参考资料。</think>### CAS乐观锁的工作原理及Java实现 #### 一、CAS基本概念 CAS(Compare-And-Swap)是一种无锁并发控制技术,核心思想是通过**硬件级别的原子操作**实现线程安全。其操作包含三个关键参数: 1. **内存地址**(V) 2. **预期原值**(A) 3. **新值**(B) 当且仅当内存地址V中的值与预期值A相等时,才会将V的值更新为B,否则不执行操作。整个过程由CPU指令(如x86的`CMPXCHG`)保证原子性[^2]。 #### 二、Java中的CAS实现 在Java中,CAS操作通过`sun.misc.Unsafe`类实现,但开发者通常直接使用`java.util.concurrent.atomic`包提供的原子类(如`AtomicInteger`): ```java // AtomicInteger的自增实现 public final int incrementAndGet() { return U.getAndAddInt(this, VALUE, 1) + 1; } ``` 底层通过**自旋循环**不断尝试CAS操作: ```java do { int prev = get(); // 读取当前值 int next = prev + 1; } while (!compareAndSet(prev, next)); // CAS尝试 ``` #### 三、CAS的三大问题与解决方案 1. **ABA问题** - 现象:变量值从A→B→A,CAS无法感知中间变化 - 解决方案:使用`AtomicStampedReference`添加版本号 ```java AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0); ref.compareAndSet(100, 200, 0, 1); // 需同时匹配值和版本号 ``` 2. **自旋开销大** - 冲突激烈时线程会长时间循环 - 优化策略:限制自旋次数或改用锁升级(如LongAdder的分段锁) 3. **单变量限制** - CAS只能保证单个变量的原子性 - 多变量场景需使用锁或`AtomicReference` #### 四、CAS与锁的对比 | | 乐观锁CAS) | 悲观锁(synchronized) | |----------|----------------------------|---------------------------| | 实现方式 | 无锁,基于版本控制 | 有锁,基于互斥 | | 适用场景 | 读多写少(如计数器) | 写操作频繁 | | 性能特点 | 无上下文切换开销 | 上下文切换开销较大 | | 典型实现 | AtomicInteger | ReentrantLock | #### 五、实际应用场景 1. 原子类操作:`AtomicInteger`计数器 2. 非阻塞算法:`ConcurrentLinkedQueue` 3. 锁状态标记:AQS(AbstractQueuedSynchronizer)中的状态变更 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

东境物语

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值