多线程——CAS篇

一、什么是CAS

1.概念

CAS全称 Compare and swap,翻译过来就是”比较并交换“,是一条硬件CPU指令,它是具有原子性——运行时不会随着线程调度产生锁冲突问题,不加锁就能够保证线程安全一个CAS硬件指令涉及到以下几个步骤:

假设寄存器中的值为A,内存中的数据为V,寄存器中需要把A修改后的值为B

比较A和V是否相等(比较)

②如果比较相等,将寄存器B值与内存变量V值进行交换——实质就是将B赋值到V(交换)

③返回操作是否成功

注:更多时候不关心寄存器中的数值是什么,更关心内存(变量)中值;

CAS是直接读写内存的,而不是操作寄存器;

2.CAS伪代码

CAS操作,是一条CPU指令,并非是以下这段代码,这个伪代码不具有”原子性“,这个伪代码只是辅助理解CAS的工作流程。

注:两种典型的 "非原子性" 的代码——运行时可能随着线程调度产生锁冲突阻塞等待
1. check and set (if 判定然后设定值) [上面的 CAS 伪代码就是这种形式]
2. read and update (i++) [之前我们讲线程安全的代码例子是这种形式]

//adress:内存V地址 //expectValue寄存器A值 //swapValue寄存器B值
boolean CAS(adress,expectVAlue,swapValue){
if(&adress ==expectValue){//如果内存变量V与寄存器A值相等
&adress=swapValue;//将寄存器B的值赋到内存变量V中
return true;
}
return false;
}

二、CAS的作用

不加锁就能够保证线程安全:当多个线程同时对某个资源进行CAS操作时,只有一个线程操作成功,但是并不会让其他线程阻塞等待,其他线程只会收到操作失败的信号

三、CAS是怎么实现的

①java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
②unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
③Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子
性。
简而言之,是因为硬件(cpu指令)予以了支持,软件层面才能做到。

四、CAS的应用

1.实现原子类

1.1原子类实例

典型的就是 AtomicInteger 类(保证++或- -时的线程安全). 其中的 getAndIncrement 相当于 i++ 操作.

AtomicInteger atomicInteger=new AtomicInteger(0);
atomicInteger.getAndIncrement();//相当于i++

1.2伪代码实现

//oldValue:线程n(n=1,2,3...依次递增)的栈内存的值 // value主内存中的值 
class Atomic Integer{
private int value;
public int getAndIncrement(){
int oldValue=value;//初始化将线程1读取到value的值到oldValue中
while(CAS(value,oldValue,oldValue+1)!=true){
oldValue=value;
}
}
//解读:若线程1的栈内存值oldvalue值与主内存//value值相同,则将oldvalue+1的值更新到主内存value中,CAS会返回true,同时循环结束;
接着当线程2的栈内存值读取value(此时已从0变为1),发现线程2的oldvalue(默认初始化为0)与value不等,因此需要进入循环,在循环中将value(此时已从0变为1)赋值到线程2自己的oldValue中(线程2的oldValue也初始化为1),最后value继续自增为2......

2.实现自旋锁

2.1CAS实现自旋锁的目的

反复检查当前锁状态是否解开了,就是为了忙等,为了能够最快速度第一时间的拿到锁~(wait/notify拿到锁的时间是随机的,不能保证第一时间拿到锁);且乐观锁(发生锁冲突低)来实现自旋锁比较合适。

问题:CAS能保证内存可见性吗?

答:不能。内存可见性,相当于是编译器把指令调整,把读内存指令调整成读寄存器指令~;但CAS本身就是一条硬件cpu指令,有读取内存的操作。

2.2自旋锁伪代码

public class spinLock{
private Thread owner=null;//记录当前的锁被哪个线程持有,当前为null没有线程持有
public void lock(){//写加锁方法
while(!CAS(this.owner,null,Thread.currentThread())){
}
//解释:情况1————比较当前线程持有者owner与null是否相同,若相同,将Thread.currentThread()设置到this.owner中,此时加锁成功,循环结束
情况二————若owner不为null(锁已经有线程持有),此时CAS什么都不干,直接返回false,一直循环,一旦访问到锁被释放就能立即获取,循环结束,坏处就是cpu会处于忙等状态
}
public void unlock(){//写解锁方法
this.owner=null;//直接将锁的持有者owner置为null
}
}

五、CAS的ABA问题

1.什么是ABA

举个例子:我们买一个手机,无法判定是新出厂的手机,还是别人用过又翻新了的手机。

由于CAS只能对比最终值是否相同,不能确定这个值中间是否经历过一个过程。退出后,无法判别出当前的值是线程T1原始获取的A还是线程T2修改过的A,不清楚。

2.ABA问题带来的弊端

大部分的情况下, t2 线程这样的一个反复横跳改动, 对于 t1 是否修改 num 是没有影响的. 但是不排除一些特殊情况.

举个例子: 

假设 滑稽老哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执行 -50 
操作.我们希望第一个线程-50成功,第二个线程-50失败

1)存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期
望更新为 50(如果获取到当前存款值不是100,就不执行)
2) 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
3) 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100 !!(ABA操作关键)
4) 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作
这个时候, 扣款操作被执行了两次!!! 显然是不符合预期的

3.如何解决ABA问题

给要修改的数据引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期. 
①如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增;

②如果发现当前版本号比之前读到的版本号大, 就认为操作失败.

举个例子:还是上述滑稽老铁取钱的例子, 给余额搭配一个版本号, 初始设为 1. 
1) 存款 100. 线程1 获取到 存款值为 100, 版本号为 1, 期望更新为 50; 线程2 获取到存款值为 100, 
版本号为 1, 期望更新为 50.
2) 线程1 执行扣款成功, 存款被改成 50, 版本号改为2. 线程2 阻塞等待中. 
3) 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100, 版本号变成3. 
4) 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 但是当前版本号为 3, 之前读
到的版本号为 1,
版本小于当前版本, 认为操作失败.

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值