写在之前
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
中的锁机制吗?
大黄:面试官您好,之前对这块有一些了解,目前我所知道的锁有几种,比如悲观锁(比如用synchronized
、ReentrantLock
),乐观锁、互斥锁等等。
大黄心想,哈哈哈,终于考到了我之前好好准备过的
synchronized
、ReentrantLock
,看我如何与面试官谈笑风生。
面试官:那你能够跟我简单说说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
本身也有很多缺点。最显著的有三个缺点:
- 循环开销大:如果比较的时候一直不等于预期值,则会一直循环等待,直到比较成功为止,该过程会给CPU带来较大开销。
- 只能保证一个共享变量的原子操作。对于多个共享变量无法保证原子性,因为每次比较的都是一个元素。
- ABA问题。
面试官:ABA问题?能简单解释一下吗?
大黄再次提醒,要从什么是ABA问题,为什么有ABA问题,如何解决等几方面考虑。
大黄:
所谓ABA
问题,比如数值i = 5
,A
线程本来想改成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
是什么?为什么要有CAS
?CAS
有什么缺点?如何解决CAS
的ABA
问题。
最后大黄分享多年面试心得。面试中,面对一个问题,大概按照总分的逻辑回答即可。先直接抛出结论,然后举例论证自己的结论。一定要第一时间抓住面试官的心里,否则容易给人抓不着重点或者不着边际的印象。
番外
另外,关注大黄奔跑,第一时间收获独家整理的面试实战记录及面试知识点总结。
我是大黄,一个只会写HelloWorld
的程序员,咱们下期见。
全部评论
(3) 回帖