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

volatile 关键字

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

可见性:当一个线程修改了 volatile 变量的值,其他线程可以立即看到最新的值。这是因为 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 expectedReference = casObject.getReference(); // 获取当前期望值

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

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

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

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

            // 将值从 A 修改为 B
            casObject.compareAndSet(expectedReference, "B", stamp, stamp + 1);
            log.info("Thread 2: Value changed to B");

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

        });

        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操作不需要加锁和解锁过程,因此减少了线程间的竞争和阻塞,提高了并发性能。悲观锁在占据资源时,其他线程访问该资源,则需要等待锁释放资源+竞争锁。

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

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

最后,「CAS + 原子类」方案其实并没有完全实现原子性操作,因为在获取stamp标记位之前这段时间里,资源发生ABA问题,那么当前的CAS就会执行更新。因此CAS+原子类并非100%线程安全。

 

在 JDK 8 中,ConcurrentHashMap 就使用了「CAS + synchronized」的方案来保证线程安全。它通过 CAS 操作实现了无锁的并发操作,并在必要时使用 synchronized 进行加锁来处理竞争情况。这种组合方案既提高了并发性能,又确保了线程安全。

CAS 主要用于以下操作:

  • 插入操作:使用 CAS 来原子地插入一个新的节点。
  • 更新操作:使用 CAS 来原子地更新一个节点的值。
  • 删除操作:使用 CAS 来原子地删除一个节点。

synchronized 主要用于以下操作:

  • 扩容操作:使用 synchronized 来保证扩容操作是原子性的。
  • 读取操作:使用 synchronized 来防止读取操作过程中发生哈希冲突导致的死循环。

JDK8 放弃分段锁(Segment)设计的原因主要有以下几个:

  • 分段锁会导致内存碎片化。
  • 分段锁会导致竞争激烈时性能下降。
  • JDK8 中使用了 CAS 等技术来提高并发性能,因此不需要分段锁。
posted @ 2023-09-26 12:11  Ashe|||^_^  阅读(97)  评论(0编辑  收藏  举报