深入了解CAS(Compare and Swap):Java并发编程的核心

什么是CAS

CAS(Compare and Swap)是一种多线程同步的原子操作,用于解决共享数据的并发访问问题。它允许一个线程尝试修改共享变量的值,但只有在变量的当前值与预期值匹配的情况下才会执行更新操作。

CAS操作包括三个主要步骤:
比较(Compare):线程首先读取共享变量的当前值,这个值通常是期望的值。

比较预期值:线程将当前值与预期的值进行比较。如果它们匹配,表示变量的当前值与线程期望的值相同。

更新(Swap):如果比较成功,线程执行更新操作,将变量的新值写入共享内存。否则,如果比较失败,线程不执行任何更新操作。

原子性(Atomicity):CAS操作是原子性的,即在执行比较和更新的整个过程中,其他线程无法中断或插入。这确保了操作的一致性。

CAS操作通常用于解决多线程并发访问共享变量时的同步问题。它允许一个线程在不需要锁的情况下,以原子的方式对共享变量进行修改。CAS是一种乐观锁(Optimistic Locking)的实现方式,它允许多个线程同时尝试修改一个变量,但只有一个线程会成功,其他线程需要重试或处理失败情况。

CAS的作用

CAS的主要作用是确保多个线程对共享变量的并发访问是线程安全的。

CAS用于代替传统锁机制,减少锁带来的性能开销和竞争,特别在高并发情况下具有显著的性能优势。

CAS避免了锁可能引发的死锁问题,因为它是一种乐观锁(Optimistic Locking)的实现方式,允许多个线程同时尝试修改变量,但只有一个线程会成功。

CAS可以实现原子操作,因此可用于实现诸如计数器递增、标志位的设置、线程安全队列的操作等。

示例

java 复制代码
import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class CASExample {
    private static final Unsafe unsafe;
    private volatile int value = 0;
    private static long valueOffset;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
            valueOffset = unsafe.objectFieldOffset(CASExample.class.getDeclaredField("value"));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public int getValue() {
        return value;
    }

    public void increment() {
        int oldValue, newValue;
        do {
            oldValue = value;
            newValue = oldValue + 1;
            //当 this中的value 和oldValue相同的时候将value更新为 newValue
        } while (!unsafe.compareAndSwapInt(this, valueOffset, oldValue, newValue));
    }

    public static void main(String[] args) {
        CASExample counter = new CASExample();

        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    counter.increment();
                }
            });
            thread.start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final Count: " + counter.getValue());
    }
}

CAS优势和局限性

优点

无锁编程:CAS操作不需要使用传统的锁机制,因此减少了锁带来的性能开销和竞争。

高性能:CAS是一种轻量级的同步机制,通常比锁具有更好的性能表现。

避免死锁:CAS操作避免了传统锁可能引发的死锁问题。

并发性:CAS允许多个线程同时尝试修改共享变量,以提高并发性。

注意事项

ABA问题:CAS可能受到ABA问题的影响,其中一个线程可能在共享变量值从A变为B再变回A时执行成功,尽管中间的状态变化可能引发问题。

自旋等待:CAS操作可能需要多次尝试才能成功,这会消耗一定的CPU资源。因此,需要设定一个最大尝试次数或者超时时间来避免无限自旋。

不适用于所有情况 :CAS适用于特定类型的原子操作,但不适用于所有并发问题。
并发性:CAS操作的并发性较高,但在高并发情况下,可能会出现多个线程竞争同一个内存位置,从而导致CAS操作的失败率上升。

ABA问题与解决方案

什么是ABA问题

ABA问题是一种在并发编程中常见的问题,它涉及到CAS(Compare and Swap)操作。ABA问题的核心是在一个线程尝试修改共享变量时,共享变量的值从A变为B,然后再变回A。这种情况可能导致CAS操作成功,尽管在中间发生了其他操作,从而引发意外的行为。

具体来说,ABA问题的情况如下:

线程T1读取共享变量的值A,并保存在本地。

在此期间,线程T2修改共享变量的值,将其从A改为B,然后再改回A。

线程T1尝试使用CAS操作将共享变量的值从A改为新值C。CAS操作成功,因为共享变量的当前值是A,与预期值A相匹配。

从CAS操作的角度来看,操作是成功的,因为共享变量的值从A变为C,尽管中间发生了A到B再到A的变化。这种情况可能在一些情况下引发问题,特别是在需要确保操作的一致性和准确性的情况下。

解决ABA问题的方案

版本号或标记:为共享变量引入版本号或标记,以跟踪变量的状态。这样,即使值从A到B再到A,版本号或标记会随之增加,CAS操作会检查版本号或标记是否匹配。

AtomicStampedReference:Java提供了AtomicStampedReference类,它允许在CAS操作中包括一个额外的整数,以跟踪变量的版本或标记。

使用锁:在某些情况下,使用传统的锁机制可以避免ABA问题。锁机制会确保一次只有一个线程可以修改共享变量。

ABA解决问题案例

java 复制代码
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABAExample {
    public static void main(String[] args) {
        // 创建一个AtomicReference,用于模拟不带版本号的CAS
        AtomicReference<Integer> atomicRef = new AtomicReference<>(100);
        
        // 创建一个AtomicStampedReference,用于模拟带版本号的CAS
        AtomicStampedReference<Integer> stampedRef = new AtomicStampedReference<>(100, 0);

        // 创建线程1,尝试进行CAS操作
        Thread thread1 = new Thread(() -> {
            int newValue = 101;
            
            // 使用AtomicReference进行CAS操作,更新值
            atomicRef.compareAndSet(100, newValue);
            
            // 使用AtomicStampedReference进行CAS操作,更新值并版本号加1
            stampedRef.compareAndSet(100, newValue, 0, 1);
            
            System.out.println("Thread 1: Value is updated to " + newValue);
        });

        // 创建线程2,模拟中间有其他线程修改过值
        Thread thread2 = new Thread(() -> {
            int newValue = 102;

            // 模拟中间有其他线程修改过值,使用AtomicReference将值设为99
            atomicRef.compareAndSet(100, 99);
            
            // 模拟中间有其他线程修改过值,使用AtomicStampedReference将值设为99,并版本号加1
            stampedRef.compareAndSet(100, 99, 0, 1);
            
            System.out.println("Thread 2: Value is updated to 99");

            // 再将值改回来,如果版本号匹配,CAS操作成功
            boolean success = atomicRef.compareAndSet(99, 100);
            boolean stampedSuccess = stampedRef.compareAndSet(99, 100, 1, 2);
            
            System.out.println("Thread 2: Value is updated back to 100: " + success);
            System.out.println("Thread 2 (Stamped): Value is updated back to 100: " + stampedSuccess);
        });

        // 启动线程1和线程2
        thread1.start();
        thread2.start();

        // 等待线程1和线程2执行完成
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出最终的值
        System.out.println("Final Value (AtomicReference): " + atomicRef.get());
        System.out.println("Final Value (AtomicStampedReference): " + stampedRef.getReference());
    }
}

在这个示例中,我们创建了两个线程,thread1和thread2。thread1首先尝试使用AtomicReference和AtomicStampedReference执行CAS操作,将值从100更新为101。然后,thread2模拟中间有其他线程修改过值,使用相同的方法将值设为99,然后将值再次修改回100。

CAS原理

1.读取内存位置的当前值。

2.检查当前值是否等于期望值。

3.如果相等,将内存位置的值更新为新值。

4.如果不相等,不做任何操作,可以重试或执行其他操作。

CAS与锁的对比

并发性

CAS具有较高的并发性,因为多个线程可以同时尝试执行CAS操作,不会阻塞其他线程。

锁的并发性较低,因为只有一个线程能够获得锁,其他线程必须等待。
自旋

CAS可能需要自旋(即多次尝试)来尝试成功,这可能会导致一定的CPU消耗。

锁使用了阻塞机制,当线程无法获得锁时,会被挂起,不会消耗CPU资源。
ABA问题

CAS可能存在ABA问题,即共享数据的值在操作过程中被其他线程改变回原始值,导致CAS操作成功,但实际数据已经发生变化。

锁不容易出现ABA问题,因为它们在获得锁时会等待,直到获得锁后再执行操作。
适用性

CAS适用于需要高并发性和较小粒度的数据更新场景,如原子变量的更新。

锁适用于复杂的临界区保护和需要确保一组操作的原子性的场景。
性能

CAS在低冲突情况下具有较高的性能,因为它允许多线程并发地进行操作。

锁在高冲突情况下可能具有更好的性能,因为它能够协调线程的执行顺序,避免争用。

相关推荐
BillKu39 分钟前
Java + Spring Boot + Mybatis 插入数据后,获取自增 id 的方法
java·tomcat·mybatis
全栈凯哥40 分钟前
Java详解LeetCode 热题 100(26):LeetCode 142. 环形链表 II(Linked List Cycle II)详解
java·算法·leetcode·链表
chxii41 分钟前
12.7Swing控件6 JList
java
全栈凯哥42 分钟前
Java详解LeetCode 热题 100(27):LeetCode 21. 合并两个有序链表(Merge Two Sorted Lists)详解
java·算法·leetcode·链表
YuTaoShao43 分钟前
Java八股文——集合「List篇」
java·开发语言·list
PypYCCcccCc1 小时前
支付系统架构图
java·网络·金融·系统架构
华科云商xiao徐1 小时前
Java HttpClient实现简单网络爬虫
java·爬虫
扎瓦1 小时前
ThreadLocal 线程变量
java·后端
BillKu2 小时前
Java后端检查空条件查询
java·开发语言
jackson凌2 小时前
【Java学习笔记】String类(重点)
java·笔记·学习