博客
关于我
CAS原子操作
阅读量:680 次
发布时间:2019-03-17

本文共 4727 字,大约阅读时间需要 15 分钟。

原子操作与CAS技术:深入理解并发控制

什么是原子操作?

所谓的原子操作,是指在多线程环境下,一个操作如果成功执行,那么它的全部效果必须得以实现,而且在这个过程中对系统状态的修改是不可被其他线程观察或干扰的,也就是说,它是一个原子性操作。这种特性对保障线程安全至关重要。

CAS(Compare And Swap)的概念

Compare And Swap(比较并交换)是一种常见的原子操作机制,特别是在处理多线程问题时非常有用。CAS在很多现代计算架构上得到广泛支持,因为它能够在不使用锁的情况下提供基本的线程安全保证。

CAS操作的核心逻辑是这样的:

  • 对比当前内存位置的值与期望的值是否相等。
  • 如果相等,则将该位置的值替换为新的值。
  • 如果不相等,则不执行替换操作,但仍然返回当前位置的值。
  • 这种机制不需要加锁,能够提供接近单线程性能的响应速度。

    Java中的原子操作类

    在Java中,AtomicInteger等原子操作类提供了多种基本原子操作:

  • get()方法: 返回当前变量的值。
  • increment(), decrement()方法: 递增或递减操作,能够原子性地完成。
  • compareAndSet(int, int)方法: 比较当前值是否等于给定值,如果等于则替换为新值,否则做 nothing。
  • 这些方法都建立在底层的至低粒度的原子操作机制上,确保了线程安全。

    实战:比对原子操作与加锁的性能表现

    为了比较原子操作和传统的加锁机制在性能上的表现,我们可以设计一个简单的累加场景:

    public class AtomicIntegerDemo {
    private static final AtomicInteger atomicInteger = new AtomicInteger(1);
    public static void main(String[] args) throws InterruptedException {
    // 测试原子操作类的性能
    testAtomic();
    // 使用加锁实现累加
    AddWithLock syncAdd = new AddWithLock();
    syncAdd.add(1000000000);
    System.out.println("使用加锁总时间: " + syncAdd.getTotalTime());
    }
    private static void testAtomic() {
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < 1000000000; i++) {
    atomicInteger.incrementAndGet();
    }
    long endTime = System.currentTimeMillis();
    System.out.println("原子操作类累加花费的时间: " + (endTime - startTime) + " 次数: " + atomicInteger.get());
    }
    }
    // 加锁实现
    class AddWithLock implements Runnable {
    private final Object lock = new Object();
    private volatile int counter = 1;
    private long totalTime = 0;
    public void add(final long count) {
    synchronized (lock) {
    for (long i = 0; i < count; i++) {
    counter++;
    totalTime += 1L;
    }
    }
    }
    public long getTotalTime() {
    return totalTime;
    }
    }

    执行结果示例:

    • 原子操作类累加: 花费约 50ms,并成功累加至 1000000001 次。
    • 加锁实现累加: 花费约 300ms,相同计数。

    结果表明,原子操作类在性能上明显优于加锁机制。

    实战:实现自定义原子递增方法

    为了更深入理解原子操作的使用,我们可以创建一个自定义的递增方法,利用原子操作类实现 thread-safe 的计数器。

    public class HalfAtomicInt {
    private AtomicInteger atomicI = new AtomicInteger(0);
    public void increment() {
    for ( ;; ) {
    int current = atomicI.get();
    boolean success = atomicI.compareAndSet(current, current + 1);
    if (success) {
    break;
    }
    }
    }
    public int getCount() {
    return atomicI.get();
    }
    }

    这个实现使用了 compareAndSet 方法,在每次递增时会自动检验当前值,并原子性地更新到下一个值。

    优缺点分析

    尽管原子操作类提供了高效、线程安全的方法,但它也有一些需要注意的地方:

  • ABA问题( Atomic Consistency Bug):
  • CAS 机制可能导致某些场景下出现不一致性问题,例如:

    • Thread-A 记录了一个值 A,并试图将其设置为 B。
    • 在这个过程中,Thread-B 可能将值从 B 换回 A。
    • 当 Thread-A 在完成自己的操作前,发现 A 已经不是原来的值了,这样就可能导致逻辑错误。

    为了解决 ABA 问题,引入了版本号机制:

    • AtomicMarkableReference: 内部保存是否需要复制的标志。
    • AtomicStampedReference: 内部保存修改的次数作为版本号。
    1. 性能开销

      循环 CAS 操作虽然效率高,但如果操作次数大量多的话,可能会产生较大的性能开销。

    2. 适用范围

      原子操作类只能直接控制一个共享变量。若需要管理多个变量或对象,需使用合成对象(如 AtomicReference)。

      例如,可以包装多个元数据到一个对象中,利用 AtomicReference 来控制这种对象的引用。

    3. ABA问题的解决方案

      为了避免 ABA 问题,Java 提供了两种改进版本的原子引用类:

    4. AtomicMarkableReference:

      • 内部添加一个标志位,用于指示是否已被修改。
      • 适用于能够通过标志位安全地比较和交换操作的场景。
    5. AtomicStampedReference:

      • 内部保存一个无锁的长整数版本号。
      • 每一次修改都会递增版本号,确保多次操作下,总能找到一致性点。
    6. 例如:

      public class AtomicStampedReferenceTest {
      public static void main(String[] args) throws InterruptedException {
      // 初始化参考对象和版本号
      AtomicStampedReference ref = new AtomicStampedReference<>("a1", 0);
      Thread t1 = new Thread(() -> {
      System.out.println("初始值:" + ref.getReference() + ";版本:" + ref.getStamp());
      boolean success = ref.compareAndSet(
      ref.getReference(),
      "a2",
      ref.getStamp(),
      ref.getStamp() + 1);
      System.out.println("t1 ComparisonAndSet 结果:" + success);
      });
      Thread t2 = new Thread(() -> {
      System.out.println("t2 查看当前值:" + ref.getReference() + ";版本:" + ref.getStamp());
      System.out.println("t2 执行compareAndSet...");
      boolean success = ref.compareAndSet(
      ref.getReference(),
      "a2",
      ref.getStamp(),
      ref.getStamp() + 1);
      System.out.println("t2 ComparisonAndSet 结果:" + success);
      });
      t1.start();
      t1.join();
      t2.start();
      t2.join();
      System.out.println("最终值:" + ref.getReference());
      System.out.println("最终版本:" + ref.getStamp());
      }
      }

      运行结果可以看到,不论是 Thread t1 还是 Thread t2,都能够正确地进行值交换,且版本号不断递增,确保一致性。

      总结

      原子操作是一种强大的工具,能够在多线程环境下确保操作的原子性,从而避免数据不一致性问题。它通过利用硬件支持的 CAS 机制,提供了高效、低开销的线程安全保障。

      优势

    7. 高性能:无需加锁,仅依赖 CPU 架.tif.c.ure支持的 CAS 指令。
    8. 线程安全:确保多线程环境下操作的原子性和一致性。
    9. 易于理解和使用:接近单线程的简洁性质。
    10. 不足

    11. 只能管理单个变量:对于多变量控制需要结合其他机制。
    12. 可能引发 ABA 问题:需要谨慎使用,特别是在多线程环境下。
    13. 性能开销:频繁使用时可能产生较高的循环开销。
    14. 通过合理的工具选择和正确的使用方法,原子操作可以在高并发场景中发挥巨大的作用。

    转载地址:http://ypkhz.baihongyu.com/

    你可能感兴趣的文章
    Node.js卸载超详细步骤(附图文讲解)
    查看>>
    Node.js基于Express框架搭建一个简单的注册登录Web功能
    查看>>
    Node.js安装与配置指南:轻松启航您的JavaScript服务器之旅
    查看>>
    Node.js安装及环境配置之Windows篇
    查看>>
    Node.js安装和入门 - 2行代码让你能够启动一个Server
    查看>>
    node.js安装方法
    查看>>
    Node.js官网无法正常访问时安装NodeJS的方法
    查看>>
    Node.js的循环与异步问题
    查看>>
    Node.js高级编程:用Javascript构建可伸缩应用(1)1.1 介绍和安装-安装Node
    查看>>
    nodejs + socket.io 同时使用http 和 https
    查看>>
    NodeJS @kubernetes/client-node连接到kubernetes集群的方法
    查看>>
    Nodejs express 获取url参数,post参数的三种方式
    查看>>
    nodejs http小爬虫
    查看>>
    nodejs libararies
    查看>>
    nodejs npm常用命令
    查看>>
    Nodejs process.nextTick() 使用详解
    查看>>
    nodejs 创建HTTP服务器详解
    查看>>
    nodejs 发起 GET 请求示例和 POST 请求示例
    查看>>
    NodeJS 导入导出模块的方法( 代码演示 )
    查看>>
    nodejs 开发websocket 笔记
    查看>>