Java 中的synchronized
是一种悲观锁,悲观锁始终假设会发生并发冲突,因此会阻止一切可能违反数据完整性的操作。而 CAS 是一种乐观锁
CAS 全称是 Compare and Swap。乐观锁假设不会发生并发冲突,因此只在提交的时候检查是否违反数据完整性,如果提交失败则会进行重试。
CAS 支持原子更新操作,适用于计数器,序列发生器等场景。CAS 操作失败时由开发者决定是继续尝试,还是执行别的操作。
CAS 的思想:
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
public class CASCase {
public volatile int value;
public void add(){
value++;
}
}
使用javap
查看编译后的add()
方法中的value++
的字节码如下:
2: getfield #2 // Field value:I
5: iconst_1
6: iadd
7: putfield #2 // Field value:I
可以看到value++
被分成了三步,虽然value
变量使用了volatile
修饰,对value++
操作禁止了指令重排序,但是value++
不是原子性操作,在多线程并发下还是会产生线程安全问题。一种解决的办法是给add()
方法加上synchronized
关键字,但是synchronized
属于悲观锁,是否能够进一步提升性能呢?其实还可以使用AtomicInteger
来满足我们的要求
public class CASCase {
public AtomicInteger value;
public void add(){
value.getAndIncrement();
}
}
在getAndIncrement()
方法底层调用的是Unsafe
类的compareAndSetInt
方法,用到的就是 CAS 的思想。
CAS 多数情况下对开发者来说是透明的。J.U.C 的 atomic 包提供了常用了原子性数据类型以及引用、
数组等相关原子类型和更新操作工具,是很多线程安全程序的首选。Unsafe 类虽然提供 CAS 服务,但是因为能够操纵任意内存地址读写而有隐患。在 Java9 之后,可以使用 Variable Handle API 来替代 Unsafe。
CAS 虽然很高效,但是也有缺点:
- CAS 若循环时间长,则开销很大。我们可以看到在
Unsafe
类的getAndAddInt()
执行时,如果 CAS 失败,会一直循环尝试进行 CAS 赋值,如果长时间都在循环,会给 CPU 带来很大开销 - CAS 只能保证一个共享变量的原子操作,但无法保证对多个共享变量的原子性操作
- ABA 问题。如果一个变量在进行 CAS 操作第一次读取时是 A,中间被其他操作改成了 B,又改回了 A,等到 CAS 操作第二次读取的时候也是 A,CAS 就会误认为这个变量从来没有被改变过。在一些场景中,ABA 问题可能会影响程序的并发正确性。为了解决这个问题,J.U.C 提供了
AtomicStampedReference
,可以通过变量值的版本来保证 CAS 的正确性。不过在 ABA 问题下,可能用传统的 加锁互斥的方法更加高效。
原文发表于:https://zhangxiann.com/archives/cas
欢迎关注我的博客:zhangxiann.com
网友评论