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(prev,next)必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果,传入原子类的变量值要用volatile修饰,以便于让其他线程更改变量后能在当前线程调用compareAndSet读取变量时及时比较。
compareAndSet(prev,next)方法一般作为模板封装为其他具体方法
模板
为什么CAS效率高
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大。但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。
CAS 乐观性体现在哪里?synchronized和Lock锁悲观性体现在哪里?
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
synchronized和Lock锁是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
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方法除了prev和next,还有期望得到的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操作。