【并发专题】CAS连环炮
本问题对应的项目链接:高频题回答模版训练
说明:一般中大厂才会问这个了,而且问的时候,可能就是连环炮,不过 CAS 的问题不算难
0、简单介绍一下 CAS,什么是 CAS ?
1、CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?
2、CAS是如何解决 ABA 问题的?
3、比起其他锁,CAS 这种锁有哪些优缺点呢?CAS 适用于哪些应用场景呢?
4、我们说了,在高并发情况下,CAS 或许效率更低,那么对此,你觉得可以怎么优化 CAS 呢?(PS:Java8 就有进行了一些优化)
共以下 6 个回答
0、简单介绍一下 CAS,什么是 CAS ?
CAS是在多线程环境下数据更新时为了保证数据原子性的机制,是基于乐观锁实现的,具体是在一个线程更新数据的时候,先在自己的工作空间将数据更新,再将数据和主内存中的数据做一个预期值和当前值的对比,相同才更新。
1、CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?
CAS的底层的原子性是通过操作系统来保障的,具体通过总线锁定和缓存锁定来保证。
2、CAS是如何解决 ABA 问题的?
在每个数据后面加一个版本号,每次更新数据的时候将版本号加一,因此当发生ABA的时候,第二个A的版本号与第一个A不同,因此被认为是两种数据。
3、比起其他锁,CAS 这种锁有哪些优缺点呢?CAS 适用于哪些应用场景呢?
CAS是乐观锁的实现,属于轻量级锁,性能很不错。缺点就是ABA问题,其次就是高并发的情况下,长时间的自旋锁会浪费CPU,CAS只能保证一个共享变量的原子性,如果需要操作多个共享变量,则考虑使用锁,或者将多个变量放在一个对象里。CAS可以实现乐观锁,比如AtomicInteger,并且java中的偏向锁和轻量级锁的底层也是CAS来实现的。
4、我们说了,在高并发情况下,CAS 或许效率更低,那么对此,你觉得可以怎么优化 CAS 呢?
高并发下主要是 CAS 自旋带来的性能问题,可以对线程加入自旋次数的限制,超过限制时可以根据情况调整自旋次数或者升级锁。还有就是 Java 8 中引入了 LongAdder 类,它用「分段计数」的方法来优化 CAS,具体来说,LongAdder 将内部的数值分为多个段,每个段都是独立的计数器。当多个线程同时更新数据时,会将这些更新分发给不同的段,减少资源竞争。
0、简单介绍一下 CAS,什么是 CAS ?
CAS是基于乐观锁的实现,可以保证一个操作的原子性来保证线程安全。全称就是比较并交换,通过将预期值和当前值进行比较,判断当前共享资源是否存在多线程竞争,如果不存在就提交更新,存在就不进行更新。
1、CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?
它实际上对应一条汇编指令cmpxchg也就是对应一条机器指令所以她可以保证原子性,它会将寄存器间的值进行比较,如果不相等就不交换两个操作数,相等就交换两个操作数
2、CAS是如何解决 ABA 问题的?
可以通过给要修改的数据加上一个版本号,这样在交换前,比较版本号是否和预期相同就可以解决ABA问题
在原子包中有AtomicStampReference类,这个类就通过引入一个int型变量stamp,每次修改引用时都会比较stamp与预期是否相同,并且在修改时必须指定新的stamp
3、比起其他锁,CAS 这种锁有哪些优缺点呢?CAS 适用于哪些应用场景呢?
CAS优点就是他是一种无锁机制去保证线程安全的,没有加锁和解锁的过程所以并发度较高,并且我们可以通过CAS操作判断当前是否发生多线程的竞争。
缺点就是ABA问题 可以通过引入版本号去解决,例如AtomicStampReference。还有就是他一次只能操作一个共享变量,但是我们可以通过使用AtomicReference将多个共享资源放在一个对象里通过修改引用来同时修改多个共享变量。还有一点就是CAS可能会因为失败不断的自旋,直到交换成功。
CAS比如可以用在对单个共享变量的修改,比如多线程环境下对int型变量进行自增。并且CAS也大量用于基于AQS实现的一些同步工具,比如ReentrantLock,在tryAcquire方法获取锁的过程中就使用CAS去尝试修改同步状态,并且在java1.8中synchronized底层偏向锁和轻量级锁获取锁的过程都会使用CAS的方式,不仅可以原子的修改同步状态,还可以判断当前是否有其他线程竞争共享资源。
4、我们说了,在高并发情况下,CAS 或许效率更低,那么对此,你觉得可以怎么优化 CAS 呢?(PS:Java8 就有进行了一些优化)
如果有大量线程进行CAS操作可能会导致不断的自旋效率很低,所以java8新增了一个LongAdder这样一个类,他通过让大量线程分散起来对不同数据进行CAS,最后累加这些数据就可以获得正确的结果。他底层使用一个cell数组来进行分段,每次进行increment操作时,都是对cell数组中的一个索引位置进行CAS自增,减轻了大量线程对同一个数据进行CAS的计算压力,并且它内部还实现了自动分段迁移机制,如果一个线程在一个分段上CAS失败后,就会自动分配到其他分段上。最好如果想要获得最后累加的值,只需要把cell数组中的值累加起来加上基数值就可以了,但是累加操作并不是原子性的,如果在计算结果时有其他的线程进行并发更新获得的结果就并不准确,所以在sum时可以考虑加锁来解决。
0、简单介绍一下 CAS,什么是 CAS ?
– CAS的全称是比较与交换,用于实现乐观锁,就是用一个预期值和要更新的变量值比较,两者相等才会进行更新
– CAS是一个原子操作,底层依赖于一条CPU的原子指令。原子操作即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。
– CAS涉及三个操作数:要更新的变量值V、预期的值E、拟写入的新值N
– 当且仅当V的值等于E时,CAS通过原子方式用新值N来更新V的值。如果不等,说明已经有其它线程更新了V,则当前线程放弃更新。
1、CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?
compareAndSet方法的实现很简单,只有一行代码。这里涉及到两个重要的对象,一个是unsafe,一个是valueOffset。
什么是unsafe呢?Java语言不像C,C++那样可以直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。
至于valueOffset对象,是通过unsafe.objectFieldOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单地把valueOffset理解为value变量的内存地址。
我们在上一期说过,CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
而unsafe的compareAndSwapInt方法参数包括了这三个基本元素:valueOffset参数代表了V,expect参数代表了A,update参数代表了B。
正是unsafe的compareAndSwapInt方法保证了Compare和Swap操作之间的原子性操作。
2、CAS是如何解决 ABA 问题的?
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 “ABA”问题。
ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。JDK 1.5 以后的 AtomicStampedReference 类就是用来解决 ABA 问题的,其中的 compareAndSet() 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
3、比起其他锁,CAS 这种锁有哪些优缺点呢?CAS 适用于哪些应用场景呢?
– CAS经常会用到自旋操作来进行重试,也就是不成功就一直循环执行到成功。如果长时间不成功,会给CPU带来很大的开销。
3. 只能保证一个共享变量的原子操作
– CAS只能对单个共享变量操作,当操作涉及多个共享变量时CAS无效。但从JDK1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,把多个变量放在一个对象里来进行CAS操作。
– 使用互斥锁来保证原子操作
4、我们说了,在高并发情况下,CAS 或许效率更低,那么对此,你觉得可以怎么优化 CAS 呢?(PS:Java8 就有进行了一些优化)
高并发下效率更低,一部分由于自旋获取线程资源等待造成的原因,可以根据当前的状态自适应调整自旋次数,而不是一致等待下去;
JDK1.8引入了Loadaddr类,采用了分段计数的来优化CAS。LoadAddr类将内部分成多个组,每个组用计数器统计,多个线程过来时分配到不同的组执行,统计。比如定义一个cell[10]数组,每个数组元素初始化为0,分段统计
0、CAS是多线程环境下一种无锁的实现,沿用乐观锁的思想,读取数据的时候不加锁,提交更新的时候判断一下数据有没有被其它线程修改过,如果没有,就提交更新;如果有,就执行解决冲突的策略,比如回滚或者重试
1、在硬件层面保证原子性。CAS的整个过程由CPU的指令集完成,它会自动检测多核处理器的缓存一致性,保证不会被其它线程中断。
2、比较数据值的同时,也比较数据的版本号,每次有线程修改数据,都会把版本号也改了,这样的话,即使数值一样,版本号不一样的话,照样可以直到数据被其它线程修改过
3、优点是免去了加锁带来的开销,修改数据的时候只需要比较预期值和版本号,可以提高竞争不激烈下的性能;
缺点是如果有很多个线程并发修改,那么可能会导致一直都失败,一直重试,占用CPU资源;导致ABA问题;只能保证一个共享变量的原子性,如果涉及到多个共享变量,就要加锁;
4、针对自旋重试次数过多,占用CPU的情况,可以设置一个重试的超时时间,避免重试得太久;
jdk1.8引入了LongAdder类来优化CAS,通过将整个计数器分成多个段,每个段都可以独立进行CAS,实现分段CAS,当多个线程同时对计数器更新时,只有更新到同一个段的线程,才会发生竞争,这样可以大大减小发生竞争的概率,提高并发性能。
这个分段的思想跟concurrentHashMap的分段锁思想类似,都是把一个大的数据结构,分成一个个小段,每个线程在这些小段上进行操作,从而减小竞争。
1.通过硬件实现原子性;
2.可以设置版本号,通过比较版本号进行操作;
3.比起其他锁,cas不需要进行线程切换,阻塞,唤醒等操作,是高效的轻量级锁,cas是一种乐观锁,适用于哪些竞争不激烈的并发场景;
4.可以设置自旋次数,达到自旋次数就升级锁;
0、简单介绍一下 CAS,什么是 CAS ?
CAS 是比较并交换的缩写,是并发编程中的一个原子操作。它包含三个操作数:内存位置、预期原值和新值。在操作期间会先比较「内存位置上的值」和「预期原值」是否相等,如果相等则交换成新值,如果不相等则不交换。
1、CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?
这个主要是通过操作系统底层硬件级别的支持,来实现 CAS 操作的原子性的。比如现代 x86 架构的处理器,是用 cmpxchg 指令将 CAS 的多个操作通过一条处理器指令实现。
再往下说,处理器指令的原子性通常由处理器提供「总线锁定」和「缓存锁定」两个机制来保证。
总线锁定本质上就是一个 LOCK# 信号,当一个处理器在总线上发出这个信号时,其它处理器的请求将被阻塞,那么此时这个处理器就可以独占共享内存。这种锁定方式的开销很大,直接把 CPU 和内存之间的通信给锁住了,导致在锁定期间其他处理器也不能操作其它内存地址的数据。
缓存锁定主要是利用了 MESI ,也就是缓存一致性协议,通过跟踪和维护各处理器缓存行的状态,确保多处理器对同一内存地址读写的一致性。这个过程是处理器直接操作数据的内存地址,不会影响其它处理器对其它内存地址的操作。
2、CAS是如何解决 ABA 问题的?
可以在每次修改值时带上版本号就行。比如 JUC atomic 包下的 AtomicStampedReference 类,通过引入一个整数戳,在对对象的引用修改时,也会改对应的整数戳,避免了 ABA 问题。
3、比起其他锁,CAS 这种锁有哪些优缺点呢?CAS 适用于哪些应用场景呢?
CAS 是乐观锁的一种实现,是非常轻量级的操作,效率很高。但也有缺点。首先就是 ABA 问题,可以带上版本号解决;还有就是循环时间长开销大,高并发情况下,自旋 CAS 如果长时间不成功,会白白浪费 CPU;同时 CAS 页只能保证一个共享变量的原子操作。当需要对多个共享变量操作时,就带考虑使用锁,或者把多个变量放在一个对象里,使用 AtomicReference 保证引用对象操作的原子性。
CAS 操作可以用于实现乐观锁,像 AtomicInteger 之类的。也可以用于一些并发容器底层同步状态的设置,比如 JDK 中 AQS 和 ReetrantLock 都有的 tryAcquire( ),都是用 CAS 操作独占式地获取并设置当前线程的同步状态。还有 Java 中的偏向锁和轻量级锁底层也用到了 CAS,作用类似。
4、我们说了,在高并发情况下,CAS 或许效率更低,那么对此,你觉得可以怎么优化 CAS 呢?(PS:Java8 就有进行了一些优化)
高并发下主要是 CAS 自旋带来的性能问题,可以对线程加入自旋次数的限制,超过限制时可以根据情况调整自旋次数。
还有就是 Java 8 中引入了 LongAdder 类,它用「分段计数」的方法来优化 CAS,具体来说,LongAdder 将内部的数值分为多个段,每个段都是独立的计数器。当多个线程同时更新数据时,会将这些更新分发给不同的段,减少了竞争。