CAS 简介

CAS 其实是我们面试中的常客,因为它是原子类的底层原理,同时也是乐观锁的原理,所以当你去面试的时候,经常会遇到这样的问题“你知道哪些类型的锁”?你可能会回答“悲观锁和乐观锁”,那么下一个问题很有可能是问乐观锁的原理,也就是和 CAS 相关的问题,当然也有可能会继续深入问你 CAS 的应用场景或者是缺点等问题。在本课时和接下来的这两个课时里,我将带领你学习如何回答这些问题。

首先我们来看一下 CAS 是什么,它的英文全称是 Compare-And-Swap,中文叫做“比较并交换”,它是一种思想、一种算法。

在多线程的情况下,各个代码的执行顺序是不能确定的,所以为了保证并发安全,我们可以使用互斥锁。而 CAS 的特点是避免使用互斥锁,当多个线程同时使用 CAS 更新同一个变量时,只有其中一个线程能够操作成功,而其他线程都会更新失败。不过和同步互斥锁不同的是,更新失败的线程并不会被阻塞,而是被告知这次由于竞争而导致的操作失败,但还可以再次尝试。

CAS 被广泛应用在并发编程领域中,以实现那些不会被打断的数据交换操作,从而就实现了无锁的线程安全。

CAS 的思路
在大多数处理器的指令中,都会实现 CAS 相关的指令,这一条指令就可以完成“比较并交换”的操作,也正是由于这是一条(而不是多条)CPU 指令,所以 CAS 相关的指令是具备原子性的,这个组合操作在执行期间不会被打断,这样就能保证并发安全。由于这个原子性是由 CPU 保证的,所以无需我们程序员来操心。

CAS 有三个操作数:内存值 V、预期值 A、要修改的值 B。CAS 最核心的思路就是,仅当预期值 A 和当前的内存值 V 相同时,才将内存值修改为 B。

我们对此展开描述一下:CAS 会提前假定当前内存值 V 应该等于值 A,而值 A 往往是之前读取到当时的内存值 V。在执行 CAS 时,如果发现当前的内存值 V 恰好是值 A 的话,那 CAS 就会把内存值 V 改成值 B,而值 B 往往是在拿到值 A 后,在值 A 的基础上经过计算而得到的。如果执行 CAS 时发现此时内存值 V 不等于值 A,则说明在刚才计算 B 的期间内,内存值已经被其他线程修改过了,那么本次 CAS 就不应该再修改了,可以避免多人同时修改导致出错。这就是 CAS 的主要思路和流程。

JDK 正是利用了这些 CAS 指令,可以实现并发的数据结构,比如 AtomicInteger 等原子类。

利用 CAS 实现的无锁算法,就像我们谈判的时候,用一种非常乐观的方式去协商,彼此之间很友好,这次没谈成,还可以重试。CAS 的思路和之前的互斥锁是两种完全不同的思路,如果是互斥锁,不存在协商机制,大家都会尝试抢占资源,如果抢到了,在操作完成前,会把这个资源牢牢的攥在自己的手里。当然,利用 CAS 和利用互斥锁,都可以保证并发安全,它们是实现同一目标的不同手段。

例子
下面我们用图解和例子的方式,让 CAS 的过程变得更加清晰,如下图所示:

假设有两个线程,分别使用两个 CPU,它们都想利用 CAS 来改变右边的变量的值。我们先来看线程 1,它使用 CPU 1,假设它先执行,它期望当前的值是 100,并且想将其改成 150。在执行的时候,它会去检查当前的值是不是 100,发现真的是 100,所以可以改动成功,而当改完之后,右边的值就会从 100 变成 150。

如上图所示,假设现在才刚刚轮到线程 2 所使用的 CPU 2 来执行,它想要把这个值从 100 改成 200,所以它也希望当前值是 100,可实际上当前值是 150,所以它会发现当前值不是自己期望的值,所以并不会真正的去继续把 100 改成 200,也就是说整个操作都是没有效果的,此次没有修改成功,CAS 操作失败。

当然,接下来线程 2 还可以有其他的操作,这需要根据业务需求来决定,比如重试、报错或者干脆跳过执行。举一个例子,在秒杀场景下,多个线程同时执行秒杀,只要有一个执行成功就够了,剩下的线程当发现自己 CAS 失败了,其实说明兄弟线程执行成功了,也就没有必要继续执行了,这就是跳过操作。所以业务逻辑不同,就会有不同的处理方法,但无论后续怎么处理,之前的那一次 CAS 操作是已经失败了的。

CAS 的语义
我们来看一看 CAS 的语义,有了下面的等价代码之后,理解起来会比前面的图示和文字更加容易,因为代码实际上是一目了然的。接下来我们把 CAS 拆开,看看它内部究竟做了哪些事情。CAS 的等价语义的代码,如下所示:

复制代码
/**
 * 描述:     模拟CAS操作,等价代码
 */
 
public class SimulatedCAS {

private int value;

public synchronized int compareAndSwap(int expectedValue, int newValue) {
        int oldValue = value;
        if (oldValue == expectedValue) {
            value = newValue;
        }
        return oldValue;
    }
}
在这段代码中有一个 compareAndSwap 方法,在这个方法里有两个入参,第 1 个入参期望值 expectedValue,第 2 个入参是 newValue,它就是我们计算好的新的值,我们希望把这个新的值去更新到变量上去。

你一定注意到了, compareAndSwap 方法是被 synchronized 修饰的,我们用同步方法为 CAS 的等价代码保证了原子性。

接下来我将讲解,在 compareAndSwap 方法里都做了哪些事情。需要先拿到变量的当前值,所以代码里用就会用 int oldValue = value 把变量的当前值拿到。然后就是 compare,也就是“比较”,所以此时会用 if (oldValue == expectedValue) 把当前值和期望值进行比较,如果它们是相等的话,那就意味着现在的值正好就是我们所期望的值,满足条件,说明此时可以进行 swap,也就是交换,所以就把 value 的值修改成 newValue,最后再返回 oldValue,完成了整个 CAS 过程。

CAS 最核心的思想就在上面这个流程中体现了,可以看出,compare 指的就是 if 里的比较,比较 oldValue 是否等于 expectedValue;同样,swap 实际上就是把 value 改成 newValue,并且返回 oldValue。所以这整个 compareAndSwap 方法就还原了 CAS 的语义,也象征了 CAS 指令在背后所做的工作。

案例演示:两个线程竞争 CAS,其中一个落败
有了这前面的等价代码之后,我们再来深入介绍一个具体的案例:两个线来执行 CAS,尝试修改数据,第一个线程能修改成功,而第二个线程由于来晚了,会发现数据已经被修改过了,就不再修改了。我们通过 debug 的方式可以看到 CAS 在执行过程中的具体情况。

下面我们用代码来演示一下 CAS 在两个线程竞争的时候,会发生的情况,同时我也录制了一段视频,你也可以直接跳过文字版看视频演示。

我们看下面的这段代码:

复制代码
public class DebugCAS implements Runnable {

private volatile int value;

public synchronized int compareAndSwap(int expectedValue, int newValue) {
        int oldValue = value;
        if (oldValue == expectedValue) {
            value = newValue;
            System.out.println("线程"+Thread.currentThread().getName()+"执行成功");
        }
        return oldValue;
    }

public static void main(String[] args) throws InterruptedException {
        DebugCAS r = new DebugCAS();
        r.value = 100;
        Thread t1 = new Thread(r,"Thread 1");
        Thread t2 = new Thread(r,"Thread 2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(r.value);
    }

@Override
    public void run() {
        compareAndSwap(100, 150);
    }
}
这里的 compareAndSwap 方法就是刚才所讲过的 CAS 的等价语义的代码,然后我们在此基础上加了一行代码,如果执行成功的话,它会打印出是哪个线程执行成功。

而在我们的 main() 方法里面,首先把 DebugCAS 类实例化出来,并把 value 的值修改为 100,这样它的初始值就为 100,接着我们新建两个线程 Thread t1 和 Thread t2,把它们启动起来,并且主线程等待两个线程执行完毕之后,去打印出最后 value 的值。

新建的这两个线程都做了什么内容呢?在 run() 方法里面可以看到,就是执行 compareAndSwap 方法,并且期望的值是 100,希望改成的值是 150,那么当两个线程都去执行 run() 方法的时候,可以预见到的是,只会有一个线程执行成功,另外一个线程不会打印出“执行成功”这句话,因为当它执行的时候会发现,当时的值已经被修改过了,不是 100 了。

首先,我们不打断点,直接执行看看运行的结果:

复制代码
线程Thread 1执行成功
150
可以看到,Thread 1 执行成功,且最终的结果是 150。在这里,打印“Thread 1 执行成功”这句话的概率比打印“Thread 2 执行成功”这句话的概率要大得多,因为 Thread 1 是先 start 的。

下面我们用 debug 的方法来看看内部究竟是如何执行的。我们先在“if (oldValue == expectedValue){”这一行打断点,然后用 Debug 的形式去运行。

可以看到,此时程序已经停留在打断点的地方了,停留的是 Thread 1(在 Debugger 里可以显示出来当前线程的名字和状态),而 Thread 2 此时的状态是 Monitor (对应 Java 线程的 Blocked 状态),其含义是没有拿到这把锁 synchronized,正在外面等待这把锁。

现在 Thread 1 进到 compareAndSwap 方法里了,我们可以很清楚地看到,oldValue 值是 100,而 expectedValue 的值也是 100,所以它们是相等的。

继续让代码单步运行,因为满足 if 判断条件,所以可以进到 if 语句中,所以接下来会把 value 改成 newValue,而 newValue 的值正是 150。

引用:https://kaiwu.lagou.com/course/courseInfo.htm?courseId=16#/detail/pc?id=302

Java多线程学习三十八:你知道什么是 CAS 吗相关推荐

  1. Java多线程学习三十六:主内存和工作内存的关系

    CPU 有多级缓存,导致读的数据过期 由于 CPU 的处理速度很快,相比之下,内存的速度就显得很慢,所以为了提高 CPU 的整体运行效率,减少空闲时间,在 CPU 和内存之间会有 cache 层,也就 ...

  2. Java多线程学习三十四:使用 Future 有哪些注意点?Future 产生新的线程了吗

    Future 的注意点 1. 当 for 循环批量获取 Future 的结果时容易 block,get 方法调用时应使用 timeout 限制 对于 Future 而言,第一个注意点就是,当 for ...

  3. Java多线程学习三十:ThreadLocal 适合用在哪些实际生产的场景中

    我们在学习一个工具之前,首先应该知道这个工具的作用,能带来哪些好处,而不是一上来就闷头进入工具的 API.用法等,否则就算我们把某个工具的用法学会了,也不知道应该在什么场景下使用.所以,我们先来看看究 ...

  4. Java多线程学习三十九:CAS 有什么缺点?

    CAS 有哪几个主要的缺点. 首先,CAS 最大的缺点就是 ABA 问题. 决定 CAS 是否进行 swap 的判断标准是"当前的值和预期的值是否一致",如果一致,就认为在此期间这 ...

  5. Java多线程学习三十五: CyclicBarrier 和 CountDownLatch 有什么不同

    CyclicBarrier 和 CountDownLatch 有什么不同? CyclicBarrier作用 CyclicBarrier 和 CountDownLatch 确实有一定的相似性,它们都能阻 ...

  6. Java多线程学习三十二:Callable 和 Runnable 的不同?

    为什么需要 Callable?Runnable 的缺陷 先来看一下,为什么需要 Callable?要想回答这个问题,我们先来看看现有的 Runnable 有哪些缺陷? 不能返回一个返回值 第一个缺陷, ...

  7. Java多线程学习二十八:原子类和 volatile 有什么异同?

    原子类和 volatile 有什么异同 案例说明 volatile 和原子类的异同 我们首先看一个案例.如图所示,我们有两个线程. 在图中左上角可以看出,有一个公共的 boolean flag 标记位 ...

  8. Java Review(三十八、网络编程)

    文章目录 网络基础 IP地址 端口 域名 网络模型 常用协议 Java 的基本网络支持 使用 InetAddress URL. URLConnection 和 URLPermission 基于TCP协 ...

  9. Java多线程学习二十:HashMap 为什么是线程不安全的

    为什么 HashMap 是线程不安全的?而对于 HashMap,相信你一定并不陌生,HashMap 是我们平时工作和学习中用得非常非常多的一个容器,也是 Map 最主要的实现类之一,但是它自身并不具备 ...

最新文章

  1. 小目标 | DAX高级实践-Power BI与Excel联合应用
  2. 数据有价——数据资产定价研究初探
  3. 安徽工业大学工商学院计算机,发个帖子(对计算机学弟学妹们的建议)
  4. Windows XP安装Python,最高支持到3.4.4,后续版本不支持
  5. 圣思园java se培训总结(82-)(观察者设计模式)
  6. nginx php默认首页,iis、apache、nginx设置默认首页index.html方法图解 电脑维修技术网...
  7. 3012C语言_数据
  8. TVP5158的多路复用技术
  9. VS2010下C/C++连接MySql数据库的方法
  10. 图书馆占座系统(七)
  11. bootdo增加验证码登陆
  12. 2019年软件评测师考试大纲
  13. 永中云编辑(java版)
  14. CRLF、CR、LF详解
  15. 用计算机怎么打出箭头,怎么打出箭头
  16. 深信服 一面 2018 秋招
  17. 关于ios苹果企业开发者
  18. 数据结构 期末复习主观题练习题(答案版)
  19. 关于光纤收发器的一些基本常识介绍
  20. 【redis源码学习】redis 中的“消息队列” Stream

热门文章

  1. 占据语音入口?苹果或将在2020年WWDC上推出SiriOS
  2. 苹果获得可折叠屏专利:可用在iPhone或其他设备上
  3. 你是否已经忘了Serlvet是什么?
  4. 我在外包的日子35:二期上线
  5. xtragrid 某个值 查找_二分查找(下):如何快速定位IP对应的省份地址?
  6. 【Flink】Flink 报错 Initial AbstractPagedOutputView Segment may not be null
  7. 【Elasticsearch】Elasticsearch gateway 流程分析
  8. 【Clickhouse】Clickhouse MergeTree家族引擎
  9. 【jvm】jvm jstack使用 Java线程Dump分析
  10. 【Elasticsearch】elasticsearch 分片