一,CAS的使用demo
我们通过一个demo来体会cas的功能是什么:
public class MyText{
//这里相当于两个integer 数字,一个是原子的,一个是普通的,我们通过++,来比较最后的值。
private AtomicInteger atomicI=new AtomicInteger(0);
private int i=0;
//使用CAS实现线程安全计数器
private void safeCount() {
for(;;) {
int ia=atomicI.get();
boolean suc=atomicI.compareAndSet(ia, ++ia);
if(suc)
break;
}
}
//非线程安全计数器
private void count() {
i++;
}
public static void main(String []args) throws InterruptedException {
final MyText text=new MyText();
List ts=new ArrayList(600);
for(int j=0;j<100;j++) {
Thread t=new Thread(new Runnable() {
public void run() {
for(int i=0;i<10000;i++) {
text.count();
text.safeCount();
}
}
});
ts.add(t);
}
for(Thread t:ts)
t.start(); //启动所以线程
//等待所以线程执行完成才输出结果;
for(Thread t:ts) {
t.join();
}
System.out.println("未上锁:"+text.i);
System.out.println("原子操作:"+text.atomicI.get());
}
}
结果: 可见两种结果相差甚远,现在你也直到了cas是做什么的了吧,它就是代替锁去实现同步操作的。保证共享数据的安全性。
二,CAS是什么?
cas是compare and swap 的缩写,比较并替换,cas有三个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
cas指令执行时,当前仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否者就什么都不做,整个比较并替换的操作是一个原子操作。
如果发现V中存的值,和预期值A不相等,那么就会提交失败,重写提交,这就叫自旋。
在这里体会什么是乐观锁,cas就觉得,并没有多少线程会去竞争这个锁,所以不会去等待,而是不断的去尝试更新值。并且认为,满足更新条件是个大概率事件。(很乐观)
三,CAS实现原理是什么?
点进方法看:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
这个就和我们lock里面发现的操作都是一样的,使用unsafe类的方法调用,这里在eclipse就看不见再里面的源码了,因为unsafe类中的方法都是native方法,由c,c++直接在操作系统上面操作。
继续跟进(我们借用大佬的图说明):
这里调用cmpxchg方法,它在Linux和windows实现不一样,我们分别看一下
linux_x86
windows_x86
cmpxchg到底在做什么?
答:os::is_MP()判断这个系统是否是多处理器,如果是多处理,返回1,否者返回0.
LOCK_IF_MP(mp) 是根据mp的值来决定是否为cmpxchg指令添加lock前缀:如果是多处理器,则需要为cmpxchg指令添加lock前缀。(这是一种优化手段,只在多核情况添加)
这里的Lock前缀指令,和处理器 实现 原子操作有关系,和内存屏障有关系。
cmpxchg源码:
jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte*dest, jbyte compare_value) {
assert (sizeof(jbyte) == 1,"assumption.");
uintptr_t dest_addr = (uintptr_t) dest;
uintptr_t offset = dest_addr % sizeof(jint);
volatile jint*dest_int = ( volatile jint*)(dest_addr - offset);
// 对象当前值
jint cur = *dest_int;
// 当前值cur的地址
jbyte * cur_as_bytes = (jbyte *) ( & cur);
// new_val地址
jint new_val = cur;
jbyte * new_val_as_bytes = (jbyte *) ( & new_val);
// new_val存exchange_value,后面修改则直接从new_val中取值
new_val_as_bytes[offset] = exchange_value;
// 比较当前值与期望值,如果相同则更新,不同则直接返回
while (cur_as_bytes[offset] == compare_value) {
// 调用汇编指令cmpxchg执行CAS操作,期望值为cur,更新值为new_val
jint res = cmpxchg(new_val, dest_int, cur);
if (res == cur) break;
cur = res;
new_val = cur;
new_val_as_bytes[offset] = exchange_value;
}
// 返回当前值
return cur_as_bytes[offset];
}
四,CAS存在的问题
1.易于理解的,循环时间长开销很大:因为cas尝试失败就会一直自旋,循环的去尝试更新。
2.只能保证一个共享变量的原子操作:对多个共享变量操作时,循环cas就无法保证操作原子性,这个时候需要用锁,:解决办法:将多个变量合成为一个变量进行操作(类的封装)
3.著名ABA问题:我们直到cas的操作前提是保证:地址v里面存放的值,和预期值A一样,那么就操作更新为B,但是如果, 有一个人把A的值,改编成为B,又从B改变回A,那么对于之前那个执行CAS操作的线程来说,是无法察觉的。解决办法:给每一次修改附上一个版本号,1.5推出了AtomicStampedReference来解决ABA问题,它的作用就是对四个标志,引用等等进行相等判断,全部都满足,才进行操作。
参考链接: https://blog.csdn.net/v123411739/article/details/79561458(有源码)
https://www.jianshu.com/p/ae25eb3cfb5d(基本使用与流程)
https://zhuanlan.zhihu.com/p/94762520?utm_source=wechat_timeline(有深度)