volatile关键字和CAS的原子性操作

发布时间 2023-09-26 12:21:07作者: ashet

volatile 关键字

volatile 是 Java 中的关键字,用于修饰变量。它的作用是确保对被修饰变量的读写操作具有可见性和顺序性。

可见性:当一个线程修改了 volatile 变量的值,其他线程可以立即看到最新的值。这是因为 volatile 变量在修改时会强制将最新的值刷新到主内存中,并在读取时从主内存中获取最新的值。

顺序性:在使用 volatile 变量进行读写操作时,编译器和处理器会禁止指令重排序,保证了操作的顺序性。

不保证原子性

CAS(Compare and Swap)

CAS 是一种无锁的原子操作,用于实现多线程环境下的并发控制。CAS 操作包含三个参数:内存位置(变量地址)、期望值和新值。它的执行过程如下:

首先,它会比较内存位置中的值与期望值是否相等。

如果相等,则将新值写入该内存位置。

如果不相等,则表示其他线程已经修改了该内存位置,CAS 操作失败,需要重新尝试。

CAS 操作是一种乐观并发控制方式,相对于传统的使用锁的悲观并发控制方式,它可以提供更高的并发性能。

CAS 存在的 ABA 问题

ABA 问题指的是在使用 CAS 进行比较时,可能出现值从 A 变为 B,然后再次变回 A 的情况。这种情况下,CAS 操作在比较时可能会成功,尽管中间的操作已经改变了变量的值。这可能引发一些潜在的问题。

此时CAS认为期望值和新值相等,会误认为可以修改变量,但其实变量已经发生了修改,CAS的原子性操作已无法保证!

为了应对ABA问题,Java 提供了 AtomicStampedReference 和 AtomicMarkableReference 类

    @Test
    void CASTest(){

        AtomicStampedReference<String> casObject = new AtomicStampedReference<>("A", 0);

        // 线程1:尝试修改值
        Thread thread1 = new Thread(() -> {
            int stamp = casObject.getStamp(); // 获取当前标记值
            String oldValue = casObject.getReference(); // 获取当前引用值

            try {
                Thread.sleep(1000); // 为了模拟thread2线程的修改
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 尝试使用 CAS 修改值
            boolean success = casObject.compareAndSet(oldValue, "C", stamp, stamp + 1);

            log.info("Thread 1 - CAS operation success: " + success);
        });

        // 线程2:修改值为初始值
        Thread thread2 = new Thread(() -> {
            int stamp = casObject.getStamp(); // 获取当前标记值
            String oldValue = casObject.getReference(); // 获取当前引用值

            // 将值从 A 修改为 B
            casObject.compareAndSet(oldValue, "B", stamp, stamp + 1);
            System.out.println("Thread 1: Value changed to B");

            // 将值从 B 修改回 A
            stamp = casObject.getStamp();
            casObject.compareAndSet("B", "A", stamp, stamp + 1);
            System.out.println("Thread 1: Value changed back to A");

            log.info("Thread 2 - Value changed to initial value");
        });

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

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

        /*
            最后输出结果为 Final value: A
            这说明thread1线程在修改变量casObject时保证了原子性,否则就会输出Final value: C
            这是因为AtomicStampedReference不仅会比较期望值,还会比较标记值stamp,当两者都相等时才会执行Swap行为修改变量
         */
        log.info("Final value: " + casObject.getReference());
    }

 由于修改AtomicStampedReference对象时,你可以在stamp标记位做标识,如果标记位的值发生了变化,那么CAS操作就会被取消,即有效避免了ABA问题。

 

关于CAS的一些思考

CAS操作允许多个线程同时访问共享资源,但只有一个线程能成功地执行更新操作。

CAS是一种乐观锁的原子操作,是因为它相对于synchronized等悲观锁实现,CAS操作不需要加锁和解锁过程,因此减少了线程间的竞争和阻塞,提高了并发性能。悲观锁在占据资源时,其他线程访问该资源,则需要等待锁释放资源+竞争锁。

悲观锁担心资源很容易被修改,为了避免多个线程看到的资源不一致,因此并发访问在锁持有资源的时候就成了串行访问。

乐观锁认为只有资源被修改时,才会发生并发安全问题,因此直接允许多线程访问资源,当更新资源时,工作内存中资源的期望值与主内存中资源的值不相等时(线程访问的资源是从主内存拷贝到线程工作区间的资源,因此才能比较),才会做出限制。