文末本文转载自微信公众号「程序员巴士」,作者tech-bus.七十一 。转载本文请联系程序员巴士公众号。
前言
自学了一年JAVA阿巴阿巴终于约到了面试,这次面试官让她谈谈对CAS的理解。
回去等通知
如果对CAS完全不了解的同学建议先去看看相关的博客了解了基本的原理,再来看面试的时候如何解答
面试官: 对CAS有了解吗?可以讲讲吗?
阿巴阿巴: 了解一些,CAS全称Compare And Swap,也就是比较和交换。
阿巴阿巴: CAS的思想比较简单,主要涉及到三个值:当前内存值V、预期值(旧的内存值)O、即将更新的内存值U,当且仅当预期值O与当前内存值V相等时,将内存值V修改为更新值U,并返回true,否则返回false。
面试官: 还有嘛?CAS的使用场景知道吗?
阿巴阿巴: 额...应该差不多了,CAS好像在并发包里使用到了。
面试官: 好,CAS有啥缺点吗?
阿巴阿巴: 额....好..好像有个ABA的问题,好像是用AtomicStampedReference解决。
面试官: 还有其他缺点吗?
阿巴阿巴: 额...记不太清了....
面试官: 行,那你这边先回去等通知哈??
阿巴阿巴: 好的~
当场发offer
面试官: CAS了解吗?讲讲
阿巴阿巴: CAS全称Compare and Swap,也就是比较和交换。
阿巴阿巴: CAS的思想比较简单,主要涉及到三个值:当前内存值V、预期值(旧的内存值)O、即将更新的内存值U,当且仅当预期值O与当前内存值V相等时,将内存值V修改为更新值U,返回true,否则返回false。
阿巴阿巴: CAS主要使用在一些需要上锁的场景充当乐观锁解决方案,一般在一些简单且要上锁的操作但又不想引入锁场景,这时候来使用CAS代替锁。
阿巴阿巴: CAS主要涉及到三个问题:ABA问题、自旋带来的消耗、CAS只能单变量
面试官: 可以详细讲一下这三个问题吗?
阿巴阿巴: ABA问题是指有一个线程t1在进行CAS操作时,其他线程t2将变量A改成了B,然后又将其改成A,这时候t1发现A并没有改变,因此进行了交换操作,由于在交换操作进行前变量A其实是有变化的,只不过最终又修改回A了,此A非彼A,这时候进行交换操作在一些业务场景下很可能要出问题,要解决ABA问题有2种方案。
阿巴阿巴: 方案一:在对变量进行操作的时候给变量加一个版本号,每次对变量操作都将版本号加1,常见在数据库的乐观锁中可见。
阿巴阿巴: 方案二:Java提供了相应的原子引用类AtomicStampedReference,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题。
阿巴阿巴: 自旋带来的消耗CAS自旋如果很长时间都不成功,这会给CPU带来很大的开销
阿巴阿巴: 解决方案:1、代码层面破坏掉for循环,设置合适的循环次数。2、使用JVM能支持处理器提供的pause指令来提升效率,它可以延迟流水线执行指令,避免消耗过多CPU资源。
阿巴阿巴: CAS只能单变量对于一个共享变量,可以使用CAS方式来保证原子操作,但是当多个共享变量时,那就无法使用CAS来保证原子性。JDK1.5开始,提供了AtomicReference类来保证引用对象之前的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
阿巴阿巴: 在JDK1.5中新增的java.util.concurrent(JUC),就是建立在CAS之上的,一般来说CAS这种乐观锁适合读多写少的场景。
面试官见阿巴阿巴对答如流,决定为难一下她。
面试官: 了解JMM吗,讲一下JMM。
阿巴阿巴: 知道一些,JMM是JAVA内存模型(JAVA Memory Model),目的是为了屏蔽各种硬件和操作系统之间的内存访问差异,从而让JAVA程序在各种平台对内存的访问一致。
阿巴阿巴: 不仅如此,JMM还规定了所有的变量都存储在主存中,每个线程都有自己独立的工作空间,线程对变量的操作必须先从主存中读取到自己的工作内存中然后再进行操作,最后回写回主存。
阿巴阿巴: 关于主存和工作内存的交互JAVA定义了八种操作来完成,且这些操作都是原子性的:lock、unlock、read、load、use、assign、store、write。
面试官: 不错不错,那JMM是真实存在的嘛,和JVM内存模型(JAVA 虚拟机内存模型)是一样的嘛?
阿巴阿巴: 不是真实存在的,JMM讲的也只是一种模型,真实的实现可能还是和模型会有差异的。JMM和JVM是不一样的,它们并不是同一个层次的划分,基本上没啥关系。
堆和方法区是线程共享的,虚拟机栈、本地方法栈、程序计数器是线程私有的。
程序计数器是这几块区域唯一一个不会发生OOM的区域。
面试官: 理解的还不错嘛,那你讲讲Volatile关键字呗。
阿巴阿巴: Volatile可以说是JAVA虚拟机提供的最轻量级的同步机制,当一个变量被定义为volatile后,它将具备俩种特性,第一个是保证此变量对所有线程的可见性,即当一个线程改变了这个变量的值后,其他线程能够立即感知的到,虽然具有可见性,但是多线程在并发情况下对volatile修饰的变量进行操作时是会有线程安全性的问题的。这是因为volatile修饰的变量在各个线程工作内存中是不存在一致性的,但是由于每次使用都要进行刷新,导致执行引擎看不到不一致的情况。
阿巴阿巴: Volatile修饰的变量的第二个特性是禁止指令重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有依赖的赋值结果的地方都能够获取到正确的结果。而不能保证赋值的顺序和代码中的书写顺序一致。例如下面的DCL的单例模式。
- public class instance {
- private String str = "";
- private volatile static instance ins = null;
- /**
- * 构造方法私有化
- */
- private instance(){
- str = "hi";
- }
- /**
- * DCL获取单例
- * @return
- */
- public static instance getinstance(){
- if (ins == null){
- synchronized (instance.class){
- if (ins == null){
- ins = new instance();
- }
- }
- }
- return ins;
- }
- }
阿巴阿巴: 如果上面ins变量不使用volatile变量进行修饰,那么当线程A在获取了instance.class锁后,对ins变量进行 ins = new instance() 初始化时,由于这是很多条指令,jvm可能会乱序执行。这个时候如果线程B在执行if (ins == null)时,正常情况下,如果为true,说明需要获取instance.class锁,等待初始化。但是这时候,假设线程A再没有对ins进行初始化完,比如只分配了空间,对象还没构造完,但是已经将引用返回了,这样线程B得到的就是一个未能实例化完全的对象,从而发生异常。而加了volatile关键字后,如果实例还未初始化完成,那么它的引用是不会向外发布的,这样即可避免异常的发生。
面试官: 不错,你这块都掌握的挺扎实的,明天可以来上班了。
阿巴阿巴: 好的??