CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
这些方法类似compareAndSwapXXX,这些操作是一个原子操作,也就是说cpu在执行cas时对这一块内存是独占排他的。在并发包中很多操作真正执行的也是cas,并发包中的类并发性能比使用 synchronized 关键字好也在于此:锁的粒度小了许多并且少了线程上下文切换。
C++源码分享:
compareAndSwapObject (jobject obj, jlong offset,jobject expect, jobject update)
{
jobject *addr = (jobject*)((char *) obj + offset);
return compareAndSwap (addr, expect, update);
}
1. 业务中需要支持并发场景,JVM支持并发两种:内置锁synchronized(悲观锁),使用原子类即CAS机制(乐观锁)
2. 在线程较少的情况下, 可以用较小的代价实现并发控制。
3. 但在线程较多情况下, CAS采用自旋锁机制(不进行线程切换,循环等待获取锁),会对CPU资源有较大浪费
本质:synchronized会导致线程状态变更,将CPU让给别的线程,如果可以很快拿到锁,遇到挫折就切换线程状态放弃CPU很浪费的; CAS是在当前线程下循环获取锁,如果长时间拿不到锁就会浪费CPU
总结:CAS一种乐观锁的思想,而且是一种非阻塞的轻量级的乐观锁,非阻塞式是指一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
谈到 CAS,基本上都要谈一下 CAS 的 ABA 问题。CAS 由三个步骤组成,分别是“读取-比较-写回”。考虑这样一种情况,线程1和线程2同时执行 CAS 逻辑,两个线程的执行顺序如下:
时刻1:线程1执行读取操作,获取原值 A,然后线程被切换走
时刻2:线程2执行完成 CAS 操作将原值由 A 修改为 B
时刻3:线程2再次执行 CAS 操作,并将原值由 B 修改为 A
时刻4:线程1恢复运行,将比较值(compareValue)与原值(oldValue)进行比较,发现两个值相等。
然后用新值(newValue)写入内存中,完成 CAS 操作
如上流程,线程1并不知道原值已经被修改过了,在它看来并没什么变化,所以它会继续往下执行流程。对于 ABA 问题,通常的处理措施是对每一次 CAS 操作设置版本号。
ABA问题的解决思路其实也很简单,就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A了。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
自旋CAS(不成功,就一直循环执行,直到成功) 如果长时间不成功,会给 CPU 带来非常大的执行开销。
当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量 i=2,j=a,合并一下 ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。
线程少用CAS,线程多用synchronized
对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态与内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
一种基于CAS的锁,获取锁的线程不会被阻塞,而是循环的去获取锁
悲观锁是基于一种悲观的态度类来防止一切数据冲突,它是以一种预防的姿态在修改数据之前把数据锁住,然后再对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作,直到前面一个人把锁释放后下一个人数据加锁才可对数据进行加锁,然后才可以对数据进行操作,一般数据库本身锁的机制都是基于悲观锁的机制实现的;
特点:可以完全保证数据的独占性和正确性,因为每次请求都会先对数据进行加锁, 然后进行数据操作,最后再解锁,而加锁释放锁的过程会造成消耗,所以性能不高;
乐观锁是对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁(这使得多个任务可以并行的对数据进行操作),只有到数据提交的时候才通过一种机制来验证数据是否存在冲突(一般实现方式是通过加版本号然后进行版本号的对比方式实现);
特点:乐观锁是一种并发类型的锁,其本身不对数据进行加锁通而是通过业务实现锁的功能,不对数据进行加锁就意味着允许多个请求同时访问数据,同时也省掉了对数据加锁和解锁的过程,这种方式因为节省了悲观锁加锁的操作,所以可以一定程度的的提高操作的性能,不过在并发非常高的情况下,会导致大量的请求冲突,冲突导致大部分操作无功而返而浪费资源,所以在高并发的场景下,乐观锁的性能却反而不如悲观锁。
系列内容:
Java多线程.线程状态_闲猫的博客-CSDN博客_java线程的状态
Java多线程.Volatile,transient,Monitor,goto_闲猫的博客-CSDN博客_java monitor
Java多线程.Unsafe_闲猫的博客-CSDN博客
Java多线程.三种实现方式_闲猫的博客-CSDN博客_java多线程实现的代码
Java多线程.ReentrantLock_闲猫的博客-CSDN博客
Java多线程.Synchronized_闲猫的博客-CSDN博客
Java多线程.LockSupport_闲猫的博客-CSDN博客
Java多线程.线程检测_闲猫的博客-CSDN博客_java 多线程测试
Java多线程.CAS_闲猫的博客-CSDN博客_java多线程cas