当前位置:   article > 正文

浅谈CAS_为什么线程竞争激烈cas就没用了

为什么线程竞争激烈cas就没用了

 

了解了解CAS

想必各位对CAS都不会很陌生,都知道CAS(Compare and Swap)是一个原子性操作,经常使用在各种JUC包下的类中。也许你还知道他有三个常见也是重要的参数 —— 内存位置(V)、预期原值(A)和新值(B)如果内存位置的值与预测的值相同就更新该内存位置上的值,否则就什么都不做,在自旋中,尝试下一次的更新操作。

 

一.为什么会出现CAS?

我们Java不是已经有语法层面的锁synchronized了嘛?再不行还有ReentrantLock,我们还要CAS这个东西干啥玩意?

 

synchronized关键字确实是一种优秀的Java语法层面的锁机制,它可以保证我们同步块中操作的原子性,可见性,有序性。并发的三大特性,它都包含了,它可以说是已经足够优秀了,但是,“优秀“是针对某些方面来说的!在有些情况下,它的表现可能就差强人意了。下面就是synchronized容易造成的问题:
 

1.我们的Java线程是映射到操作系统的内核态线程上实现的,所以我们的加锁,解锁,挂起等操作都是需要从用户态切换到内核态实现的,而且还可能涉及线程上下文的频繁切换,如果这时线程的执行时间很短,可能会出现我们花在线程调度上的时间超过线程执行的时间。这将是性能上的硬伤。

2.如果一个线程持有锁,其它线程就必须阻塞等待该线程释放锁。

3.如果一个低优先级的线程获取了锁,那么高优先级的线程需要等待其释放锁,这将导致优先级倒置,引起性能风险。

4.不正确的加锁容易导致线饥饿,死锁等问题。

 

正是因为基于阻塞锁的机制在进行原子性操作的时候会造成类似于上面的各种各样的问题,所以我们的CAS机制就出现了!

 

二.CAS的原理

 

1.什么是CAS?

CAS是一种典型的乐观锁的应用,我们的CAS主要包含两个步骤:冲突检测和数据更新。当多个线程尝试使用CAS同时更新内存中的同一变量时,只有一个线程可以更新变量的值,其它线程都会失败,失败的线程并不会挂起线程,而是什么也不做,并开始下一次尝试。

 

2.关于CAS的思索。

这里需要思索一下:我们上面说CAS是基于冲突检测与数据更新的,“冲突检测和数据更新”这不是两个操作嘛?两个操作要如何保证其原子性问题?难道通过synchronized来加锁嘛?这不是自己逗自己玩呢嘛。那么CAS如果保证这两个操作的原子性呢(可能在指令集方面对应多个指令)?

 

3.并发>Java

在Java之前,并发已经早早的应用在服务器领域了,所以并发并不是Java特有的,所以我们尽量不要站在Java的角度看待并发。因为并发的广泛应用,所以一些硬件厂商早早的就在芯片中加入了许多直指并发操作的原语。从而在硬件层面上保证了原子性,提高效率。我们的CAS在Intel芯片上就是由cmpxchg指令来保证其原子性的。它的作用是将指定内存地址的内容与所给的某个值相比,如果相等,则将其内容替换为指令中提供的新值,如果不相等,则更新失败。

 

 

三.CAS J.U.C的基石

 

1.JNI的出现,使J.U.C浮出水面

在Java发展的初期,并不能利用硬件提供的这些遍历来提高并发程序的性能。而随着Java的发展,Java的本地方法(JNI)的出现,使得Java程序越过JVM成为一种便捷,因此,Java在并发上的手段也多了起来。而在Doug Lee所提供的concurrent包中,CAS理论是他实现整个并发包的基石。

 

2.J.U.C发布

在JDK1.5新增加的java.util.concurrent(J.U.C Java并发工具包)就是建立在CAS之上的。相比synchronized这种阻塞用法,CAS是非阻塞用法的一种实现。所以在J.U.C刚刚出现的时候性能上,功能上都是碾压synchronized的(不过在jdk1.8以后已经基本持平了)。

 

3.再谈J.U.C

要说把,CAS只能算是J.U.C的一半基石,另一半就是我们的Java语法层面的volatile关键字。我们都知道,volatile关键字是Java原生语法层面的关键字,也是一种最轻量的锁机制。根据Java内存模型就可以知道,volatile可以保证共享变量在内存中的可见性与有序性,但是无法保证原子性,所它就和可以保证原子性的CAS操作合作,打造了J.U.C的王朝!

 

 

 

 四.CAS存在的问题。

 

1.ABA问题

我们都知道CAS操作一般需要三个参数内存位置(V)、预期原值(A)和新值(B)。如果出现这么一种情况:C线程先拿取内存位置V上的值,这时C线程被打断,A线程占据CPU,A线程完成了修改V上的值的全过程,然后B线程获得CPU执行权限,又将A修改后的值改了回去,这时C醒了过来,那么在C线程看来,内存位置V上的值是没变的,所以它进行了修改,但是实际上内存V上的值早已不是原来的值,这违反了CAS操作的初衷,造成了ABA问题。

解决方法:给每一个内存位置上的值添加版本。根据 版本+值  来判断内存位置V上的值是否被改变过。

J.U.C下提供了相关原子操作类的使用:

AtomicMarkableReference:处理ABA问题,其中的版本戳类型是Boolean类型。关注的重点是当前共享变量是否被修改过。

AtomicStampedReference:处理ABA问题,其中的版本戳类型是计数类型。关注的重点是有几个人动过共享变量。

 

2.长时间自旋。造成CPU资源的浪费

并发度太高,CAS操作很难获得成功,CPU不断循环,资源大量浪费。

 

3.只能保证一个共享变量的原子操作

只能保证一个共享变量的原子操作(可以将多个对象包装成一个对象,然后使用AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。)

 

 

五.CAS与Synchronized的使用背景

 

1.对资源竞争较少的情况

线程冲突较轻的情况下,使用synchronized进行共享变量的同步操作,由于频繁的加锁解锁操作,线程上下文的切换,会导致CPU资源的极大浪费。而CAS基于硬件实现,不需要从目态切入内核态,因为线程冲突较轻,所以自旋次数不会太多,因此可以获得较好的性能。

 

2.对于资源竞争严重的情况下

线程冲突严重的情况下,CAS自旋的概率会大大增加,从而浪费更多的CPU资源,这时建议使用synchronized关键字。

 

补充: synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

 

 

 

 

 

完!

 

 

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/311411
推荐阅读
相关标签
  

闽ICP备14008679号