深入了解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());
    }
}

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());
    }
}

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

你可能感兴趣的:(并发编程,java,jvm)