Java原子操作CAS原理解析

作者:ねぇ 时间:2021-10-20 01:07:05 

一、CAS(Compare And Set)

Compare And Set(或Compare And Swap),CAS是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)、新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。

在java中可以通过锁和循环CAS的方式来实现原子操作。Java中 java.util.concurrent.atomic包相关类就是 CAS的实现,atomic包里包括以下类:

AtomicBoolean可以用原子方式更新的 boolean 值。
AtomicInteger可以用原子方式更新的 int 值。
AtomicIntegerArray可以用原子方式更新其元素的 int 数组。
AtomicIntegerFieldUpdater基于反射的实用工具,可以对指定类的指定 volatile int 字段进行原子更新。
AtomicLong可以用原子方式更新的 long 值。
AtomicLongArray可以用原子方式更新其元素的 long 数组。
AtomicLongFieldUpdater基于反射的实用工具,可以对指定类的指定 volatile long 字段进行原子更新。
AtomicMarkableReferenceAtomicMarkableReference 维护带有标记位的对象引用,可以原子方式对其进行更新。
AtomicReference可以用原子方式更新的对象引用。
AtomicReferenceArray可以用原子方式更新其元素的对象引用数组。
AtomicReferenceFieldUpdater<T,V>基于反射的实用工具,可以对指定类的指定 volatile 字段进行原子更新。
AtomicStampedReferenceAtomicStampedReference 维护带有整数“标志”的对象引用,可以用原子方式对其进行更新。

二、AtomicInteger

AtomicInteger可以用原子方式更新的 int 值。AtomicInteger 可用在应用程序中(如以原子方式增加的计数器),并且不能用于替换 Integer。但是,此类确实扩展了 Number,允许那些处理基于数字类的工具和实用工具进行统一访问。 我们拿 AtomicInteger为例来学习下 CAS操作是如何实现的。

通常情况下,在 Java中,i++等类似操作并不是线程安全的,因为 i++可分为三个独立的操作:获取变量当前值,为该值+1,然后写回新的值。在没有额外资源可以利用的情况下,只能使用加锁才能保证读-改-写这三个操作时“原子性”的。但是利用加锁的方式来实现该功能的话,代码将非常复杂及难以维护,如:


synchronized (lock) {
 i++;
}

相关类中还需要增加 Object lock等额外标志,这样就带来了很多麻烦,增加了很多业务无关代码,给开发与维护带来了不便。
然而利用 atomic包中相关类型就可以很简单实现此操作,以下是一个计数程序实例:


public class Counter {
 private AtomicInteger ai = new AtomicInteger();
 private int i = 0;

public static void main(String[] args) {
   final Counter cas = new Counter();
   List<Thread> threads = new ArrayList<Thread>();
   // 添加100个线程
   for (int j = 0; j < 100; j++) {
     threads.add(new Thread(new Runnable() {
       public void run() {
         // 执行100次计算,预期结果应该是10000
         for (int i = 0; i < 100; i++) {
           cas.count();
           cas.safeCount();
         }
       }
     }));
   }
   //开始执行
   for (Thread t : threads) {
     t.start();
   }
   // 等待所有线程执行完成
   for (Thread t : threads) {
     try {
       t.join();
     } catch (InterruptedException e) {
       e.printStackTrace();
     }
   }
   System.out.println("非线程安全计数结果:"+cas.i);
   System.out.println("线程安全计数结果:"+cas.ai.get());
 }

/** 使用CAS实现线程安全计数器 */
 private void safeCount() {
   for (;;) {
     int i = ai.get();
     // 如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值
     boolean suc = ai.compareAndSet(i, ++i);
     if (suc) {
       break;
     }
   }
 }
 /** 非线程安全计数器 */
 private void count() {
   i++;
 }
}
/**
非线程安全计数结果:9942
线程安全计数结果:10000
*/

其中非线程安全计数器所计算的结果每次都不相同且不正确,而线程安全计数器计算的结果每次都是正确的。

三、存在的问题

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题:ABA问题、循环时间长开销大、只能保证一个共享变量的原子操作。

ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。 从Java1.5开始JDK的 atomic包里提供了一个类AtomicStampedReference 来解决ABA问题。这个类的 compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

来源:https://www.cnblogs.com/lee0527/p/11688125.html

标签:java,原子,操作,cas
0
投稿

猜你喜欢

  • 一款域名监控小工具 Domain(IP)Watcher 实现代码

    2023-09-15 11:55:44
  • Mybatis实现Mapper动态代理方式详解

    2023-08-13 08:37:41
  • SpringMVC使用MultipartFile实现文件上传

    2021-07-01 03:50:22
  • 让C# Excel导入导出 支持不同版本Office

    2023-01-11 05:30:53
  • Java 8 Stream操作类型及peek示例解析

    2021-07-17 20:42:08
  • 浅谈Java基础知识之BigDecimal

    2021-09-06 16:49:27
  • j2ee之AJAX二级联动效果

    2021-09-13 10:06:58
  • Springboot中加入druid连接池

    2023-08-08 05:47:22
  • spring boot 配置动态刷新详解

    2023-09-26 10:24:42
  • SpringBoot整合第三方技术的详细步骤

    2023-11-29 08:22:48
  • Java回调方法详解

    2022-09-25 08:33:54
  • 8种android 对话框(Dialog)使用方法详解

    2023-07-09 18:23:44
  • springboot+mybatis配置控制台打印sql日志的方法

    2023-12-15 15:40:55
  • Android图片识别应用详解

    2022-01-13 15:37:31
  • C#使用WebClient实现上传下载

    2022-04-22 21:46:31
  • C++作用域与函数重载的实现

    2022-04-30 06:28:30
  • springboot反爬虫组件kk-anti-reptile的使用方法

    2022-01-09 14:12:59
  • Mybatis关联查询结果集对象嵌套的具体使用

    2021-07-12 22:09:18
  • android实现扫码枪功能

    2022-08-28 21:33:35
  • Android性能优化之RecyclerView分页加载组件功能详解

    2023-06-04 16:24:14
  • asp之家 软件编程 m.aspxhome.com