CAS 详解

CAS 是什么

CASCompare And Swap 的缩写,即比较并交换,其底层使用的是 UnsafeNative 方法,该方法是使用 C 语言中的方法调用 CPU 的读取,比较 并 交换 这三个系统指令,组成的一个原子操作,当读取到的值跟预期的值一致时就交换,不一致就不交换。

CAS 的不足

  1. 只能保证一个变量操作的原子性
  2. CAS 可能会循环时间过长, CPU 开销比较大
  3. 使用 CAS 过程中可能会发生 ABA 的问题

CAS 中的 ABA 问题

ABA 问题是指,如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。

对于 A-B-A 问题,通常的处理措施是对每一次 CAS 操作设置版本号。在变量前面追加版本号,每次变量更新就把版本号加 1,则 A-B-A 就变成 1A-2B-3A

Javajava.util.concurrent.atomic 包下,提供了 AtomicMarkableReferenceAtomicStampedReference 类来解决 A-B-A 问题。其 compareAndSet()方法首先检查当前引用是否等于预期引用,并且还会检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用的该标志的值设置为给定的更新值。

悲观锁与乐观锁

  1. 悲观锁:悲观锁是非常悲观,每次修改数据的时候都认为别人会同时修改数据,会发生冲突。因此操作数据时会直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。
  2. 乐观锁:乐观锁是非常乐观,每次修改数据的时候都认为别人不会同时修改数据。因此乐观锁不会锁住数据,只是在执行更新的时候判断一下在此期间别人是否修改了数据;如果别人修改了数据则放弃此次更新操作,否则执行更新操作。

synchronized 是悲观锁,这种线程一旦得到锁,其他需要锁的线程就需要挂起。
CAS 操作就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,CAS 是无阻塞模型。

CAS 的简单实例

不加锁,线程不安全实例

package com.aha.train.test.lock.cas;

/**
 * 测试线程安全的问题
 *
 * @author WT
 * @date 2021/10/12
 */
public class Test01 {

    private static int count = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep(10);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                //每个线程让count自增100次
                for (int j = 0; j < 100; j++) {
                    count++;
                }
            }).start();
        }

        try{
            // 3s 之后查看 count 的值 让多线程给跑完
            Thread.sleep(3000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(count);
    }

}

请问 count 的输出值是否为 1000 ?

答案是否定的,因为这个程序是线程不安全的,所以造成的结果 count 值可能小于 1000;
那么如何改造成线程安全的呢,其实我们可以使用上 Synchronized 同步锁,我们只需要在 count++ 的位置添加同步锁,代码如下:

package com.aha.train.test.lock.cas;

/**
 * 测试线程安全的问题
 *
 * @author WT
 * @date 2021/10/12
 */
public class Test01 {

    private static int count = 0;

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep(10);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                //每个线程让count自增100次
                for (int j = 0; j < 100; j++) {
                    synchronized (Test01.class) {
                        count++;
                    }
                }
            }).start();
        }

        try{
            Thread.sleep(2100);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(count);
    }

}

加了同步锁之后,count 自增的操作变成了原子性操作,所以最终的输出一定是 count=100 ,代码实现了线程安全。

但是 Synchronized 虽然确保了线程的安全,但是在性能上却不是最优的,Synchronized 关键字会让没有得到锁资源的线程进入 BLOCKED 状态,而后在争夺到锁资源后恢复为 RUNNABLE 状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。

尽管 Java1.6Synchronized 做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。

这边更优的一个操作就是就是使用原子类操作,所谓原子类指的是 java.util.concurrent.atomic 包下,一系列以 Atomic 开头的包装类。例如AtomicBooleanAtomicIntegerAtomicLong 。它们分别用于 BooleanIntegerLong 类型的原子性操作。

private static AtomicInteger count = new AtomicInteger(0);

public static void main(String[] args) {
    
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            try {
                Thread.sleep(10);
            } catch (Exception e) {
                e.printStackTrace();
            }
            // 每个线程让count自增100次
            for (int j = 0; j < 100; j++) {
                // 返回的是新值  还有一个 getAndIncrement 是返回旧值然后加一
                count.incrementAndGet();
            }
        }).start();
    }

    try{
        Thread.sleep(2100);
    }catch (Exception e){
        e.printStackTrace();
    }
    
    System.out.println(count);
}

原子引用

对于基础类型的包装类,JUC 包下提供了 AtomicInteger 这种原子类,如果是自定义的类如何能实现原子操作呢?这边 JUC 提供了 AtomicReference<V> , 具体操作可以参考下面的代码:

@AllArgsConstructor
class User {
    int age;
    String name;

    public static void main(String[] args){
        User user = new User(20,"张三");
        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(user);
    }
}

原子引用设置版本号解决 ABA 的问题

package com.aha.train.test.lock.cas;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * 测试 CAS 的 ABA 问题
 * @author WT
 * @date 2021/10/21
 */
@Slf4j
public class ABADemo {

    static AtomicStampedReference<String> atomicReference = new AtomicStampedReference<>("A", 1);

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                // 睡一秒,让 t1 线程拿到最初的版本号
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicReference.compareAndSet("A", "B", atomicReference.getStamp(), atomicReference.getStamp() + 1);
            atomicReference.compareAndSet("B", "A", atomicReference.getStamp(), atomicReference.getStamp() + 1);
        }, "t2").start();
        new Thread(() -> {
            //拿到最开始的版本号
            int stamp = atomicReference.getStamp();
            try {
                // 睡3秒,让t2线程的ABA操作执行完
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("是否将值设置成功:{}",atomicReference.compareAndSet("A", "C", stamp, stamp + 1));
        }, "t1").start();
    }

}
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 在JDK 5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁 锁机制存在以下问题: (1...
    SunnyMore阅读 5,079评论 2 18
  • 转自:https://blog.csdn.net/ls5718/article/details/52563959j...
    onlyHalfSoul阅读 1,187评论 0 7
  • 一、术语定义 二、CAS应用 CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值...
    二月夜阅读 1,050评论 0 0
  • CAS在底层源码中是使用非常广的,像我之前的HashMap源码解析、volatile详解等文章都有提到CAS。本文...
    贪挽懒月阅读 5,992评论 6 13
  • 一. 书面概述 CAS的全称为Compare And Swap,直译就是比较交换。是一条CPU的原子指令,其作用是...
    657455400阅读 420评论 0 1

友情链接更多精彩内容