并发知识-- CAS
本文最后更新于:4 个月前
CAS (Compare And Swap) (CompareAndSet): 比较并交换,比较并设定
CAS 是一种并发时用到的技术,其本质是一种算法
为了解决并发时修改共享变量的问题,传统的解决方式就是加锁,例如典型的 synchronized
关键字,但是加锁一般都是悲观锁,即首先认为每次都会进行多线程的竞争,所以首先加锁,其他线程需要阻塞直到锁释放
但是加锁可能会带来额外的性能开销,如果是对于轻量级的并发,或者只是线程交替执行,就可能显得很笨重
那么,乐观锁应运而生,而它的一个实现就是 CAS
乐观锁是首先认为没有其他线程会对同一个数据进行改变,而当真正去操作共享变量的时候,再去看共享变量有没有被更新,如果没有就进行一个原子性的业务操作(通过硬件支持将非原子性的操作变成原子性),反之操作失败
CAS 的思想当然也和上面的类似,只是具体实现细节需要我们探究
Java 中实现 CAS 的是 Unsafe 类,其中有一个本地方法
compareAndSwapObject(Object var1, long v2, Object v4, Object v5);
其中 var1 就是要改变的类对象,v2 则是要修改的字段的内存地址,v4 则是未修改之前的预期值,v5 则是新的值
这个方法是通过 JNI 调用 Unsafe.cpp 文件的一个函数,而这个函数(在Linux 版本下)又是汇编语言的cmpxchg
指令,所以实际上是硬件层面上提供的 API 实现的
CAS 产生的问题:
-
只能对单一共享变量操作
如果需要对多个变量进行 CAS 同步更新,那么还是得加锁解决
-
ABA
如果一个线程 CAS 将 A 值改变成了 B,另一个线程再将 B 改变成了 A,然后又有一个线程看到的依然是 A,就会认为这个值一直是 A,没有改变,此时在将 A 改为 C,这样就覆盖了之前两个线程的操作
-
解决办法是: 增加一个字段来标识整个对象的版本,一旦进行了修改,就改变版本号
JDK 的实现: AtomicStampedReference
将对象和一个版本戳组成一个内部类 pair,相比没有版本戳的例如 AtomicInteger 每次更新还需要传入预期的版本戳
-
-
如果用到了 CAS + 自旋
如果 CAS 不成功,会一直自旋重试导致 CPU 占用资源
甚至类似产生死锁的问题,那么就会一直占用,更损耗性能
所以要根据情况选择是否要加入自旋,分场景考虑
本博客所有文章除特别声明外,均采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 。转载请注明出处!