• 深入了解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可以实现原子操作,因此可用于实现诸如计数器递增、标志位的设置、线程安全队列的操作等。

    示例

    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());
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    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解决问题案例

    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());
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    在这个示例中,我们创建了两个线程,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在低冲突情况下具有较高的性能,因为它允许多线程并发地进行操作。
    锁在高冲突情况下可能具有更好的性能,因为它能够协调线程的执行顺序,避免争用。

  • 相关阅读:
    在自定义数据集上实现OpenAI CLIP
    JSON.stringify()与Qs.stringify()区别 应用场景
    【SpringCloud微服务项目实战-mall4cloud项目(3)】——mall4cloud-auth
    Redis高级篇——Redis的优化
    数位DP day45
    网上有什么可以做的副业,或者是挣钱的方法?
    如何优雅的写 Controller 层代码?
    SpringCloud Alibaba整合Ribbon负载均衡
    JAVA设计模式
    中秋之际献上【中秋快乐】藏头诗
  • 原文地址:https://blog.csdn.net/qq_41956309/article/details/133846003