乐趣区

关于java:Offer快到碗里来囊中之物CAS

微信公众号:大黄奔跑
关注我,可理解更多乏味的面试相干问题。

写在之前

Hello,大家好,我是只会写 HelloWorld 的程序员大黄。

明天是 2020 最初一天,这应该是往年的最初一条推文。《时代周刊》将往年评为“2020 是最蹩脚一年”,纵是事实千疮百孔,生存还是持续向前。借着最初一条推送祝大家新年快乐,找工作的同学如愿找到现实的工作。

前篇文章咱们回顾了什么是 volatile 关键字,曾经从是什么、为什么、有什么用等三个方面开展,如果还有什么不懂的能够私我,交换交换。

明天是 Java 并发编程知识点面试串讲第二弹——CAS

对于并发编程一些源码和深层次的剖析曾经举不胜举,大黄想要从面试角度与大家交换该如何答复 CAS 知识点,次要的目标是让大家相熟知识点、并且在面试那么短时间内答复好面试官的问题,从而顺利拿到 Offer。

面试问题概览

依照国际惯例,大黄会先整顿一些真正面试中对于 CAS 被问到的问题,次要目标是想让大家理解,这类问题面试官会如何问。做到 “ 胸中有丘壑、下笔如有神 ”。

能够先看看这些面试题目,当初心中想想,如果当面试中真的遇到这些题目,该如何答复呢?

1.CAS 原理说一下【阿里巴巴一面(大黄年老时校招亲身经历)】
2. 乐观锁 CAS、乐观锁 synchronized 和 ReentrantLock、实现原理以及区别【阿里巴巴】
3.CAS 有什么问题,如何解决呢【美团、滴滴】
4.CAS 具体怎么实现【快手、滴滴】
5.CAS 操作和锁哪个效率更高,在任何状况下都是 CAS 更快吗?【字节跳动】
6. 对于 CAS 谈谈你的认识【拼多多】

CAS原理真的是各个公司面试中曾经成为了必考的面试题目。这种题目既有区分度、又有深度,答复好了实现成为 offer 收割机。本篇文章想让大家当前面对 CAS 问题时,丝毫不慌,称为送分题。

面试回顾

前半场面试下来,面试官根底题也考查的差不多了,顺其自然的提到了锁,个别提到了锁的时候,面试官必定不会走马观花的考查,这里的面试内容泛滥,如是顺其自然有了以下对话:

面试官:大黄同学是吧,之前有理解过 Java 中的锁机制吗?

大黄:面试官您好,之前对这块有一些理解,目前我所晓得的锁有几种,比方乐观锁(比方用synchronizedReentrantLock),乐观锁、互斥锁等等。

大黄心想,哈哈哈,终于考到了我之前好好筹备过的synchronizedReentrantLock,看我如何与面试官谈笑自若。

面试官:那你可能跟我简略说说 Java 中的乐观锁机制吗?

我擦,怎么不依照套路来呢,我还认为要问乐观锁呢,不过对于乐观锁,咱们也不慌,毕竟面试之前可是看过大黄《Offer 快到碗里来系列》啊。

大黄:对于乐观锁,是零碎一种比拟乐观的状态,认为线程中运行时大概率不会产生资源竞争的状况,因而不会运行的时候就开始获取锁,只有等到有资源竞争的时候才会获取锁,Java中个别通过 CAS 机制来解决。

面试官:能跟我简略说说什么是 CAS 吗?

大黄:
书面上的话:比拟并替换(Compare And Swap),次要是为了解决原子性问题,同时又不想利用重量级的锁。
艰深的话说:如果想更新一个值,想看看该值是否等于某个值,如果等于则更新该值。
我能够简略举一个例子:

大黄可见性 Demo 演示小插曲

public class Juc005Cas {public static void main(String[] args) {AtomicInteger integer = new AtomicInteger(10);
        // compareAndSet 如果更新胜利,则返回 true,如果更新失败,则返回 false
        System.out.println(integer.compareAndSet(10,11));
        System.out.println(integer.get());
        // 这一次数值曾经变成了 11,先比拟发现,诶,库外面不是 10,间接跳过
        System.out.println(integer.compareAndSet(10,15));   
        System.out.println(integer.get());
    }
}

上面是运行后果:
后果如下:

true
11
false
11

面试官:嗯嗯,你方才说到 CAS 次要是为了解决原子性问题,CAS为什么能够保障原子性呢?

大黄:在 CAS 底层中,通过判断内存中某个地位的值是否为预期值,如果是则更新为新的值,该比拟和赋值动作是原子的。是齐全依赖于硬件的性能,通过底层硬件实现原子的性能,该过程的执行是不容许中断的,不会造成所谓的数据不统一问题。

大黄小讲堂

咱们能够看看 CAS 底层实现逻辑:

CAS自身通过本地(native)办法来调用,该类能够间接操作底层内存的数据,Unsafe中所有的办法都是用 native 润饰,能够间接调用操作系统底层资源执行工作。

其中变量 valueOffset 示意该对象值在内存中的偏移地址,用来寻找数据的地址。

/**
 * 该办法自身是调用 unsafe 中的 compareAndSwapInt()办法
 * this 示意以后对象 
 * valueOffset 内存地址
 * expect 内存中的值
 * update 更新值
 * 
 **/
public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// unsafe.class
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

咳咳,小课堂下课了,面试持续。

面试官:既然 CAS 就能够解决原子性问题,那是不是能够这么说并发编程中能够不须要 synchronized 或者 ReentrantLock 了?是不是对象的原子性保障用 CAS 就够了。

个别面试官如果这么问题的话,必定是想问某个技术是否有无缺点。

大黄:不是的。因而 CAS 自身也有很多毛病。最显著的有三个毛病:

  1. 循环开销大:如果比拟的时候始终不等于预期值,则会始终循环期待,直到比拟胜利为止,该过程会给 CPU 带来较大开销。
  2. 只能保障一个共享变量的原子操作。对于多个共享变量无奈保障原子性,因为每次比拟的都是一个元素。
  3. ABA 问题。

面试官:ABA 问题?能简略解释一下吗?

大黄再次揭示,要从什么是 ABA 问题,为什么有 ABA 问题,如何解决等几方面思考。

大黄:
所谓 ABA 问题,比方数值 i = 5A 线程原本想改成 10,在更改之前,B线程先将 i 先由 5 变成了 6,再更新成 5,然而 A 线程并不知道 i 产生过扭转,依然将 i 改成 10。只管最初的后果没有问题,然而整个过程还是不对的。

我能够写一个例子来阐明一下。

/**
 * ABA 问题复现
 **/
public class Juc005CASImprove {static AtomicReference<Integer> reference = new AtomicReference <>(100);
    public static void main(String[] args) {new Thread(()->{reference.compareAndSet(100,111);
            reference.compareAndSet(111,100);
        },"Thread One").start();
        new Thread(()->{
            try {TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            // 还是能够更新胜利
            System.out.println(reference.compareAndSet(100,222)+" "+ reference.get());
        },"Thread Two").start();}
}

面试官:那如果非要用 CAS 来保障多个共享变量的原子性有方法做吗?

大黄:
有的,能够采纳 AtomicReference 来解决,能够将多个变量作为一个对象放入到 AtomicReference 中。

大黄小 demo

public class Juc005CASAbaProblem {public static void main(String[] args) {User a = new User("a",12);
        User b = new User("b",12);
        AtomicReference<User> userReference = new AtomicReference <>();
        userReference.set(a);
        System.out.println(userReference.compareAndSet(a,b) +" "+userReference.get().toString());
    }
}

面试官:方才说到的 ABA 问题,行业内有没有一些通用的做法呢?如何解决呢?

大黄:个别都是利用工夫戳作为 版本,保障每次批改的值都是举世无双的。能够将元素与版本号形成一个对象,每次保障援用的原子性。

大黄有话说:答复解决 ABA 问题的时候,顺便记得提一下数据的乐观锁也能够用版本问题解决,所谓的版本,就是每次更新数据之前,看看所批改的数据是否是预期的数据。比方有一条记录为 id = 111, age=12,想要改成 age=22。大略 sql 如下
update user set age = 22 where id = 111 and age = 12

大黄可见性 Demo 演示小插曲

public class Juc005CASImprove {static AtomicReference<Integer> reference = new AtomicReference <>(100);
    /**
     * 初始化工夫值及初始化工夫戳
     */
    static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference <>(100,1);
    public static void main(String[] args) {
        /**
         * 利用第一个线程模仿 ABA 问题
         */
        new Thread(()->{int initStamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"第 0 次版本号:"+initStamp);
            try {TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            // 模仿 ABA 问题 100->101->100
            System.out.println(Thread.currentThread().getName()+"第一次版本号:"+stampedReference.getStamp());
            stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"第二次版本号:"+stampedReference.getStamp());
            stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"第三次版本号:"+stampedReference.getStamp());
        },"Thread Three").start();
        /**
         * 利用另一个线程尝试批改
         */
        new Thread(()->{int initStamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"第 0 次版本号:"+initStamp);
            try {TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            Boolean result = stampedReference.compareAndSet(100,2019,initStamp,stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"是否更新胜利,"+result);
            System.out.println(stampedReference.getReference());
        },"Thread Four").start();}
}

到这里,一个 CAS 问题,曾经和面试官差不多聊了十多分钟,原来的送命题是不是变成了送分题了。

面试官:好了,明天的面试就到这里,请问你下一场面试什么时候有工夫呢,我来安顿一下。

哈哈哈,祝贺你,到了这里面试曾经胜利拿下了,此刻要克服住心田的喜悦淡定的说到。

大黄:我这几天都有工夫的,看你们的安顿。

总结

自身次要围绕结尾的几个真正的面试题开展,简略来说,CAS是什么?为什么要有 CASCAS 有什么毛病?如何解决 CASABA问题。

最初大黄分享多年面试心得。面试中,面对一个问题,大略依照总分的逻辑答复即可。先间接抛出论断,而后举例论证本人的论断。肯定要第一工夫抓住面试官的心里,否则容易给人抓不着重点或者不着边际的印象。

番外

另外,关注大黄奔跑公众号,第一工夫播种独家整顿的面试实战记录及面试知识点总结。

我是大黄,一个只会写 HelloWorld 的程序员,咱们下期见。

关注大黄,充当 offer 收割机

退出移动版