目录
▮CAS
CAS全名:compare and swap。他是CPU上的一条指令。这条指令的逻辑是:
比较内存R和寄存器A的值,若相等,则交换内存R和寄存器B的值
。不过,在实际中,我们更加关注内存上的值,所以这个操作可以换种逻辑:
比较内存R和寄存器A的值,若相等,则给R赋值为B
。
创造出来这么个东西是为了什么呢?因为,
CAS具有原子性
,上面的比较和赋值是一步同时完成的,它只是CPU上的一条指令。一步完成又有什么作用呢?因为,
CAS能不加锁就能实现线程安全,减少了锁的开销
。要知道,在多线程中,这种比较和赋值中是很容易出现线程安全问题的,CAS的原子性,就是它最大的价值。
以下就是CAS的伪代码实现,注意是伪代码实现逻辑
boolean CAS(内存R, 寄存器A, 寄存器B) {
if (R == A) {
R = B;
return true;
}
return false;
}
•CAS在JVM里的实现原理(博主表示看不懂)
1.java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
2.unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
3.Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子 性。
▮基于CAS创造出来的原子类
CAS指令最大的价值就是它的原子性,利用这点,我们可以创造出许多带有原子性的东西。
▪AtomicInteger:原子整型
int的增大和减小操作都是非原子的
,涉及:获取值,计算值,赋予值,这三个CPU指令。这在多线程当中是容易出现安全问题的。所以,我们使用CAS,利用它的原子性来解决这个问题
•示例:后置++操作
以后置++为例,感受一下CAS的魅力。以下是代码实例,其中getAndIncrement()方法就代表着后置++操作。
class AtomicInteger {
//赋予的值
private int value;
//此方法代表着后置++操作
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
这里要注意一点,这里面使用了while()循环。这是因为,在多线程环境中,oldValue刚刚得到了value的值后,value的值就可能会被其它线程所修改。此时的CAS操作也不会成功,所以使用while循环重复执行,在value值被修改后,重新赋值给oldValue。
了解了后置++原理后,简单的写个操作示例
•AtomicInteger的其它方法
(转自:http://t.csdn.cn/8l3pS)
▪自旋锁的实现
自旋锁所在的线程能围着一个锁对象一直请求,尽可能的在第一时间就拿到这个锁对象。但在判断锁对象是否有锁这方面,在多线程中是会出现问题的。因为判断对象是否有锁,涉及:取值、比较、赋值,这三个指令。
此线程可能在判断锁对象是否上锁的过程中,锁对象被其它线程上锁,而导致此线程误判,去给一个已经上了锁的对象来上锁
。
下面就是自旋锁实现的代码。
public class SpinLock {
//持有锁对象的线程,为null则表示锁对象没有被上锁
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
}
可以看到,自旋锁会一直循环执行CAS,这是要消耗系统资源的。
▮CAS的aba问题
int A = 100;
线程t1的执行逻辑是CAS(A,100,50)
线程t2的执行逻辑是CAS(A,100,50)
线程t1执行后,A就变成了50,而线程t2的CAS就会执行失败。如果有其它线程,在t1把A改成50后,又把A改成100,使得t2得以执行。线程t2这里就可能会出现BUG,因为线程t2可能不该执行这个CAS。
aba问题就是一个值从a变成了b,又从b变回了a。虽然CAS命令依旧可以执行,但这个变换过的A值,可能在逻辑上出现了一些错误
。
举个示例。小明卡里的余额还有100,小明想去ATM里取走50。在第一次取钱操作时,ATM卡了一下,取钱迟迟没有反应;对此,小明开始了第二次取钱操作,在他刚刚操作好第二次前,第一次取钱操作被启动了,卡里的余额只剩50,那么这第二次取钱操作就会失败。这是一个正常的情况,问题是,如果在第一次取钱操作执行后,小明的朋友又给小明卡里打了50块钱,卡里的余额又变成了100,那么第二次取钱操作就会成功执行,这下就会执行两次取款操作,跟小明的预期不符,就会产生BUG。
▪解决方案
给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期
。
CAS 操作在读取旧值的同时, 也要读取版本号.。真正修改的时候, 如果当前版本号和读到的版本号相同, 则修改数据,并把版本号 + 1。 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了
就比如上例中,小明的两次取钱操作的版本都是:扣钱01。在第一次取钱操作后,版本变成“取钱02”,那么第二次取钱操作的版本就会对不上,从而执行失败。
在 Java 标准库中提供了 AtomicStampedReference 类. 这个类可以对某个类进行包装, 在内部就提供了上面描述的版本管理功能. 关于 AtomicStampedReference 的具体用法此处不再展开. 有需要的自行查找。
▮相关面试题
来自比特就业课的课件,做了一个搬运。