当前位置: 首页 > 科技观察

《IllustratedJavaConcurrency》面试必问的CAS原理你懂吗?

时间:2023-03-13 13:29:22 科技观察

本文转载自微信公众号《爱笑的建筑师》,作者雷小帅。转载本文,请联系LoveSmile的架构师公众号。在并发编程中,我们都知道i++操作不是线程安全的,因为i++操作不是原子操作。如何保证原子性?常用的方法是加锁。在Java语言中,可以使用Synchronized和CAS来达到加锁的效果。synchronized是悲观锁。线程执行的第一步是获取锁。一旦获取到锁,其他线程进入后就会阻塞等待锁。如果不好理解,我举个生活中的例子:人进厕所后,先锁上门(获取锁),然后开始上厕所。这时候别人来了只能在外面等(堵),再急也没用。如厕后打开(解锁)门,其他人可以进入。CAS是一种乐观锁。线程在执行过程中不会被锁定。假设没有冲突完成一个操作,如果因为冲突导致失败,则重试,直到成功。什么是CAS?CAS(Compare-And-Swap)意思是比较和交换。它是一个CPU并发原语,用于判断内存中的某个值是否是预期值,如果是,则将其更改为新值。这个过程是原子的。让我们用一个小例子来解释。CAS机制中使用了三个基本操作数:内存地址V、旧期望值A、计算后要修改的新值B。(1)初始状态:变量值1存放在内存地址V。(2)线程1想把内存地址为V的变量值加1,此时对于线程1来说,旧的期望值A=1,待修改的新值B=2。(3)在线程1即将提交更新之前,线程2先走了,已经先将内存地址V中的变量值更新为2。(4)线程1开始提交更新,首先将期望值A与内存地址V的实际值进行比较(Compare),发现A不等于V的实际值,提交失败。(5)线程1重新获取内存地址V的当前值,重新计算要修改的新值。此时,对于线程1,A=2,B=3。这个重试过程称为旋转。如果多次失败,将进行多次旋转。(6)线程1再次提交更新,这次没有其他线程改变地址V的值。线程1进行Compare,发现期望值A与内存地址V的实际值相等,进行Swap操作将内存地址V的实际值修改为B。总结:更新变量时,只有当变量的期望值A与内存地址V中的实际值相同时,内存地址V对应的值才会为改为B。这整个操作就是CAS。CAS的基本原理CAS主要包括两个操作:Compare和Swap。有人可能会问:这两个操作能保证是原子的吗?是的。CAS是系统原语,属于操作系统的语言。原语由若干条指令组成,用于完成某一功能的一个过程。原语的执行必须是连续的,执行过程中不允许中断,也就是说,CAS是CPU的原子指令,由操作系统硬件来保证。在IntelCPU上,使用cmpxchg指令。回到Java语言,JDK在1.5版本之后引入了CAS操作,在sun.misc.Unsafe类中定义了CAS相关的方法。publicfinalnativebooleancompareAndSwapObject(Objecto,longoffset,Objectexpected,Objectx);publicfinalnativebooleancompareAndSwapInt(Objecto,longoffset,intexpected,intx);publicfinalnativebooleancompareAndSwapLong(Objecto,longoffset,longexpected,longx);可以看到该方法被声明为native,如果你熟悉C++,可以自己下载OpenJDK的源码查看unsafe.cpp,这里就不分析了。CAS在Java语言中的应用在Java编程中,我们通常不直接使用CAS,而是通过JDK封装的并发工具类间接使用。这些并发工具类都在java.util.concurrent包中。J.U.C是java.util.concurrent的缩写,又名JavaConcurrentProgrammingToolkit,面试经常考,非常非常重要。目前CAS主要用于JDK中J.U.C包下的Atomic相关类。比如AtomicInteger类可以解决i++的非原子问题。通过查看源码可以发现,主要是通过volatile关键字和CAS操作来实现的。具体原理和源码分析会在后面的文章中进行分析。CAS问题CAS不是万能的,有很多问题。敲黑板:CAS的题目有哪些?这是面试的高频考点,需要掌握。典型ABA问题ABA是CAS运算的经典问题。假设有一个变量,其初始值为A,更改为B,然后再次更改为A。这个变量其实已经被修改了,只是CAS操作未必能感知到。如果是整形的话,不会影响最终的结果,但是如果对象的引用类型包含多个变量,引用中包含的变量没有变化,又被修改了,就会造成很大的问题。如何解决?这个想法其实很简单。在变量前加上版本号,变量每更新一次,版本号就加一。结果如下:最终结果是A但是版本号变了。从JDK1.5开始提供了AtomicStampedReference类。该类的compareAndSe方法首先检查当前引用是否等于期望引用,当前标志是否等于期望标志。如果全部相等,则引用和标志的值自动设置为给定的指定更新值。自旋开销问题自旋操作将在CAS冲突发生后开始。如果资源竞争非常激烈,自旋很长时间都不能成功,会给CPU带来非常大的开销。解决方案:可以考虑限制自旋次数,避免过度消耗CPU;你也可以考虑延迟执行。只能保证单个变量的原子性。当对一个共享变量进行操作时,可以使用CAS来保证原子性,但是如果要对多个共享变量进行操作,CAS就不能保证原子性了。例如i和j同时加1:i++;j++;这时候可以使用synchronized来加锁。还有别的办法吗?是的,多个变量操作合并为一个变量操作。从JDK1.5开始,提供了AtomicReference类来保证被引用对象之间的原子性。您可以将多个变量放在一个对象中以执行CAS操作。有态度的总结CAS就是CompareAndSwap,它是一个CPU原语,其原子性由操作系统保证。Java语言从JDK1.5版本开始引入CAS,是Java并发编程J.U.C包的基石,应用广泛。当然,CAS也不是万能的,还有很多问题:典型的ABA问题,自旋开销问题,只能保证单个变量的原子性。