CAS(Compare and Swap)比较并交换
乐观锁与悲观锁
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样当第二个线程想拿这个数据的时候,第二个线程会一直阻塞,直到第一个释放锁,他拿到锁之后才可以访问。Java中的synchronized的实现就是一种悲观锁。
乐观锁:每次拿数据的时候都认为别的线程不会修改这个数据,所以不会上锁,但是在更新的时候会判断一个在此期间别的线程有没有修改过数据,乐观锁适用于读操作多的场景,这样可以提高程序吞吐量。在JUC atomic包下的原子变量就是使用了乐观锁的一种实现方式CAS。
乐观锁的实现方式:CAS
在JDK1.5之前同步都是依靠synchronized关键字保证的,无论哪个线程持有共享变量的锁,都会采用独占的方式来访问这些变量。在这种情况下存在以下问题:
- 在多线程竞争下,加锁、释放锁会导致较多的上下文切换和调度延时,引起性能问题。
- 如果一个线程持有锁,其他线程都会挂起,等待持有锁的线程释放锁。
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能风险。
对比于悲观锁的这些问题,另一个更加有效的锁就是乐观锁。
乐观锁
每次不加锁而是假设没有并发冲突去操作同一变量,如果有并发冲突导致失败,则重试直至成功。
相对于悲观锁而言,乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则返回错误信息让用户决定如果去做。
CAS具体实现
实现细节:冲突检查和数据更新。当多个线程尝试使用CAS同时更新一个变量时,只有一个线程可以更新变量的值,其他线程都会失败,失败的线程并不会挂起,而是再次尝试。
CAS操作包括三个操作数:需要读写的内存位置V、预期原值A、新值B。如果内存位置与预期原值A相匹配,那么将内存位置的值更新为新值B。如果内存位置与预期原值不匹配,那么处理器不会做任何操作。无论哪种情况,他都会在CAS指令之前返回该位置的值。(在CAS的一些特殊情况下将仅返回CAS是否成功,而不提取当前值)
具体的实现可以查看之后的博文: JUC原子操作篇:Atomic原子引用类
CAS的缺陷
ABA问题
因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题,比如一个旧值A变成了B,然后再变成A,这时执行CAS操作会发现旧值没有改变还是A,但实际上的确发生了变化。解决方案可以沿袭数据库中乐观锁的方式,添加一个版本号来解决。
对应Java中的实现为AtomicStampedReference
可以参考: JUC原子操作篇:Atomic原子引用类
自旋时间长开销大
自旋CAS(不成功,就一直循环执行,直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升。
pause两大作用
- 可以延迟流水线执行指令(depipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
- 可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
CAS与synchronized的使用情景
- 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
- 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
注:synchronized在jdk1.6之后,已经改进优化了。底层实现主要依靠Lock-Free的队列,基本思路是自选后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能,而线程冲突严重的情况下,性能远高于CAS。