CAS乐观锁

CAS乐观锁:

该操作由sun.misc.Unsafe类里面的compareAndSwapInt()compareAndSwapLong()compareAndSwapObject()等几个方法包装提供。J.U.C包里面的原子类, 例如compareAndSet()和getAndIncrement()等方法都使用了Unsafe类的CAS操作来实现。

 

CAS是比较并交换由Unsafe类的compareAndSwap方法提供,能保证修改变量操作的原子性。底层是 lock cmpxchg 指令(X86 架构),当执行到 lock 的指令时, 会把总线锁住,当cpu执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,又借助 了volatile保证了多个线程对内存操作的准确性,是原子的。(总线是计算机底层传送信息的公共通信干线,分为地址总线、数据总线等)

CAS必须借助 volatile才能保证原子性。

 

CAS主要包含三个参数(偏移量表示要更新的变量在内存中的位置,pre旧值表示根据之前获得的情况预期可能获得的值,next表示更改后的新值), 如果运行该方法的时候当前线程获取到的值和prev相同,就把该变量修改为next,如果该方法成功修改返回true,没有修改成功,运行方法时的变量值和之前获取到的变量值prev不一致,说明有别的线程修改了变量,返回false失败之后不会进入阻塞,而是会修改预期值再次尝试。

 

 

compareAndSet(prevnext)必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果,传入原子类的变量值要用volatile修饰,以便于让其他线程更改变量后能在当前线程调用compareAndSet读取变量时及时比较。

 

 

 

compareAndSet(prev,next)方法一般作为模板封装为其他具体方法

模板

 

 

为什么CAS效率高

无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大。但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

 

CAS 乐观性体现在哪里?synchronizedLock锁悲观性体现在哪里?

CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。

synchronizedLock是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

 

CAS的使用场景?

CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思:因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一CAS适合线程数不远高于CPU数的情况,因为如果竞争激烈,可以想到自旋重试必然频繁发生,反而效率会受影响 。

 

 

 

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题:ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作

 

1.  ABA问题。CAS方法只会判断第一个参数的值是不是和该线程用get()方法获取时候的值一样,但并不能确定该共享变量是不是被其他线程多次修改。

ABA问题的解决思路是使用AtomicStampedReference类,该类有一个stamp参数,代表版本号。如果其他线程成功进行CAS操作,stamp参数会改变,在当前线程进行CAS操作时即使共享变量值是期望的,如果版本号不同于自己之前获得的也无法成功执行CAS

解决:

AtomicStampedReference类,该类初始化时除了共享变量额外还有一个stamp参数,代表版本号。该类的CAS方法除了prev和next,还有期望得到的版本号和成功修改后的版本号。如果其他线程成功进行CAS操作,stamp参数会改变,在当前线程进行CAS操作时即使共享变量值是期望的,如果版本号不同于自己之前获得的也无法成功执行CAS。

 

AtomicMarkableReference类,有时候并不关心引用变量更改了几次,只是单纯的关心是否更改过,可以用AtomicMarkableReference类。该类初始化时除了共享变量额外还有一个mark布尔参数,代表是否被修改过。该类的CAS方法除了prevnext,还有期望得到的mark值和成功修改后的mark值。如果其他线程成功进行CAS操作,第一次操作将mark改变,后边再修改可以把两个mark参数设置为一样的;在当前线程进行CAS操作时即使共享变量值是期望的,如果mark不同于自己之前获得的也无法成功执行CAS

 

2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率

3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。Java1.5开始JDK提供了AtomicReference来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值