一、CAS 面试题
1. 说说CAS、CAS有什么问题(ABA)?(美团一面)
Compare And Swap 对比交换(原子指令)
CAS是 CPU指令 操作系统原生 API,JVM对它进行了封装(C++),供我们使用。
通过判断 内存 和 寄存器(预期值) 中的值是否一致,来决定是否执行 写指令,改变内存的值
判断在赋值前是否有 其他线程 改变了 内存 中的值,一致就说明没有改变,通过赋值改变内存中的值。不一致就返回,预期值改变 去进行下一次循环。
2. CAS如果失败会发生什么?(科大讯飞Java后端)
CAS一般搭配循环使用,循环中每次CAS失败后,预期值会被更新为最新的内存值!!!
然后进入循环再次进行CAS操作
二、原子类
利用CAS原子的特性,对读写操作的一体操作,封装成一个个类
它们对基本 数据类型 int double.....进行了封装 ,提供一些作用与 ++ ,+=,这类操作的方法,区别是它们操作是原子的,所以不会出现线程安全问题,我们常用的是 Integer 和 Long 类型。
1. 常用方法:
2. 代码示例:
三、CAS实现自旋锁
1. 伪代码:
通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就⾃旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
四、ABA问题(详解)
我们已经知道了CAS这条指令的基本原理,就是判断当前内存中的值是否和我们预期的值一致,来判断是否有其他线程已经修改过内存中的值,成功就执行写操作,失败就改变预期值,从而实现原子的特性,这一切看着很合理。
但是它有个缺陷:ABA问题,这个问题就是说,我们在判断是否与预期值一致的时候,如果已经有个线程将内存中的 A 改成别的 B ,又将它改成 A ,CAS是察觉不到的,这就是ABA问题。
一般这也不会出大问题,但是有极端情况
举个栗子:
假设你的银行卡内有1000元,现在你往出转账500元,ATM机卡了,你狂点两下,意外产生两个线程,都是要-500元
第一个线程:内存值是1000,预期值是1000,判断相等后,执行-500的操作,
第二个线程:内存变成500,预期值1000,不相等,不执行
上方线程二出现问题,结束,这是完美结局。
假如!!!!情况变了,第一个线程执行完,中间的时候有人给你转账500,
线程二读内存发现 内存是1000(500+500),预期是1000,🆗了直接-500........
这样的结果就是,你卡里有1000,转出去500,进去500,一看余额不是1000了,变成500了,寄了。。。。。
虽然我们说,这个情况太巧合了,正好出来两个线程,正好两个线程 中间 加了 和 扣款 相同数目的钱,可是再小的概率,这件事也一定会出现,我们就提出了针对ABA问题的解决策略。
1. 解决策略:
对内存中的值赋予一个 版本号,版本号 只加不减 ,每经历一次操作成功,版本号++,如果发现接下来的操作中,版本号 高于我们 预期的版本号,就不执行操作。
拿我们刚刚的例子来说:我们给余额一个版本号 并且初始化为 1 ,经过第一次取钱,版本号变成2
经过别人转给你,变成3,等到第二个线程,版本号为2才执行,发现你这里是3,直接不执行了
通常会采用一些方法来增加版本号的唯一性,比如使用时间戳、随机数等来初始化版本号。
但是时间戳的使用也要谨慎,因为我们的地球公转时间和自转时间不是标准的365倍,所以我们要每四年为一个闰年,这一年会多一天,但是我们为了计算机的时间准度,我们会执行闰秒,也就是手动把时间往回调1秒这样,会每隔一段时间执行闰秒。根据的是国家授时中心的数据,往往这一秒就会导致一些问题,所以时间虽然是正向流逝的,但是也有倒流的情况,用时间戳要谨慎......
时间倒流