CAS简单介绍

本文探讨了并发编程中如何通过synchronized和CAS实现锁机制,对比了悲观锁(如synchronized)与乐观锁(如CAS),强调了CAS在读多写少场景的优势以及ABA问题、自旋开销和多变量操作的处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

并发编程中,如何保证原子性?

常见的做法就是加锁。

在 Java 中,我们可以使用 synchronized 关键字和 CAS(Compare-and-Swap) 来实现加锁效果。

synchronized 是悲观锁,线程开始执行第一步就要获取锁,一旦获取锁,其他的线程进入后就会阻塞并等待锁。例如平时上厕所,一个人去后将门锁上,其他人来了只能在外面等待。

CAS 是乐观锁,线程执行的时候不会加锁,它会假设此时没有冲突,然后完成某项操作;如果因为冲突失败了就一直重试,直到成功为止。

乐观锁与悲观锁

锁可以从不同的角度来分类。比如 synchronized 的四种锁状态分别为无锁、偏向锁、轻量级锁、重量级锁。同理,乐观锁和悲观锁也是一种分类方式。

悲观锁

悲观锁,顾名思义,它是悲观的,总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作都加上锁,以保证临界区的程序同一时间只能有一个线程在执行。

乐观锁

乐观锁,听名字就知道它很乐观。它总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。一旦多个线程发生冲突,乐观锁通常使用一种称为 CAS 的技术来保证线程执行安全。

  • 乐观锁多用于 “读多写少” 的环境,避免频繁加锁影响性能。
  • 悲观锁多用于 “写多读少” 的环境,避免频繁失败和重试影响性能。

什么是 CAS

CAS 全称为 Compare And Swap 翻译过来就是比较并且交换。它是一种 CPU 并发原语,用于实现多线程环境下的同步操作。

CAS 通常包含三个参数:当前内存值 V、旧的预期值 A、即将更新的值 B。它的执行过程如下:

  1. CPU 读取内存值 V。
  2. CPU 将读取到的值和 旧的预期值 A 比较。
  3. 如果结果相等,就将内存值修改为 B,并返回 true,否则,什么都不做,并返回false。

CAS 基本原理

CAS 主要包括两个操作:CompareSwap ,那这两个方法如何保证原子性?

我们知道,在 Java 中,如果一个方法是 native 的,那 Java 就不负责具体实现它,而是交给底层的 JVM 使用 C 语言或者 C++ 去实现。

JDK 在 1.5 版本之后引入了 CAS 操作,在 sun.misc.Unsafe 这个类中定义了 CAS 相关的方法。如下图:
在这里插入图片描述
可以看到,方法被声明为 native

在 Intel 的 CPU 中,使用 cmpxchg 指令在 CPU 上完成 CAS 操作的,但在多处理器情况下,必须使用 lock 指令加锁来完成。当然,不同的操作系统和处理器在实现方式上肯定会有所不同。

CMPXCHG是“Compare and Exchange”的缩写,它是一种原子指令,用于在多核/多线程环境中安全地修改共享数据。CMPXCHG在很多现代微处理器体系结构中都有,例如Intel x86/x64体系。对于32位操作数,这个指令通常写作CMPXCHG,而在64位操作数中,它被称为CMPXCHG8B或CMPXCHG16B。

CAS 在 Java 语言中的应用

在 Java 编程中我们通常不会直接使用到 CAS,都是通过 JDK 封装好的并发工具类来间接使用的,这些并发工具类都在 java.util.concurrent (简称 JUC,即Java 并发编程工具包) 包中。

目前 CAS 在 JDK 中主要应用在 J.U.C 包下的 Atomic 相关类中。

在这里插入图片描述

CAS 的三大问题

ABA 问题

ABA 问题是 CAS 操作的一个经典问题,即假设一个变量初始值为 A,被修改为B,然后又被修改为A。这个变量实际被更新了两次,但是 CAS 是检查不出来的。

案例:

import java.util.concurrent.atomic.AtomicReference;

public class StackExample {
    private static AtomicReference<Node> top = new AtomicReference<>();

    public static void main(String[] args) {
        Node node1 = new Node("Node 1");
        Node node2 = new Node("Node 2");
        Node node3 = new Node("Node 3");

        top.set(node1);  // 初始栈顶为 node1

        // 线程1执行出栈操作,期望栈顶元素为 node1
        Thread thread1 = new Thread(() -> {
            Node expected = top.get();
            System.out.println("Thread 1: Expected top = " + expected);

            // 模拟线程1执行过程中,线程2将栈顶元素修改为 node2
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            top.compareAndSet(expected, node2);  // 将栈顶元素修改为 node2
            System.out.println("Thread 1: Actual top after CAS = " + top.get());

            // 线程1再次将栈顶元素修改为 node1
            top.compareAndSet(node2, expected);
            System.out.println("Thread 1: Actual top after second CAS = " + top.get());
        });

        // 线程2执行入栈操作,在线程1执行过程中将栈顶元素修改为 node2,并再次修改回 node1
        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            top.compareAndSet(node1, node3);  // 将栈顶元素修改为 node3
            System.out.println("Thread 2: Actual top after CAS = " + top.get());

            top.compareAndSet(node3, node1);  // 将栈顶元素修改回 node1
            System.out.println("Thread 2: Actual top after second CAS = " + top.get());
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class Node {
        private String data;

        public Node(String data) {
            this.data = data;
        }

        @Override
        public String toString() {
            return data;
        }
    }
}

在上述代码中,线程1执行出栈操作,期望栈顶元素为 node1。然而,在线程1执行过程中,线程2将栈顶元素先后修改为 node3 和再次修改回 node1。这样,虽然线程1的 CAS 操作在值比较时是成功的,但实际上栈顶元素已经发生了变化,从而导致 ABA 问题。

输出结果可能如下:

Thread 1: Expected top = Node 1
Thread 2: Actual top after CAS = Node 3
Thread 1: Actual top after CAS = Node 2
Thread 1: Actual top after second CAS = Node 1
Thread 2: Actual top after second CAS = Node 1

如何解决? 思路其实很简单,在变量前加版本号或时间戳。

从 JDK 1.5 开始,JDK 的 atomic 包里提供了一个AtomicStampedReference类来解决 ABA 问题。这个类的 compareAndSe方法首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
在这里插入图片描述

自旋开销问题

CAS 出现冲突后就会开始自旋操作,如果资源竞争非常激烈,自旋长时间不能成功就会给 CPU 带来非常大的开销。

解决方案:可以考虑限制自旋的次数,避免过度消耗 CPU;另外还可以考虑延迟执行。

CAS 只能够保证一个共享变量的原子操作

当对一个共享变量执行操作时,可以使用 CAS 来保证原子性,但是如果要对多个共享变量进行操作时,CAS 是无法保证原子性的,比如需要将 i 和 j 同时加 1:

i++;j++; 

这个时候可以使用 synchronized 进行加锁,有没有其他办法呢?有,将多个变量操作合成一个变量操作。从 JDK1.5 开始提供了AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

总结

CAS 是 Compare And Swap,是一条 CPU 原语,由操作系统保证原子性。

Java语言从 JDK1.5 版本开始引入 CAS , 并且是 Java 并发编程J.U.C 包的基石,应用非常广泛。

当然 CAS 也不是万能的,也有很多问题:典型 ABA 问题、自旋开销问题、只能保证单个变量的原子性。


感谢大家读到这里,后续还会有其他相关文章,欢迎继续阅读。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值