java并发编程-CAS

java / 2022-08-22
0 1,411

一、CAS是什么

cas全称compare and swap,比较并交换,是一条CPU并发原语。解决多线程环境下使用锁导致上下文切换导致性能消耗的一种机制。它的功能是判断内存中某个地址的值是否是期望的值,如果是就修改为新的值,整个过程是原子的。

这是一种非阻塞算法,线程在获取资源失败时,不需要挂起,因此省去了上下文切换带来的性能损耗。

CAS并发原语体现在java中就是Unsafe类中的方法。调用Unsafe类中的方法,JVM会自动实现出CAS汇编指令。

二、CAS原理

首先先来看一段代码。

public class CasTest {
    public static void main(String[] args) {
        // num 初始值 = 5
        AtomicInteger num = new AtomicInteger(5);
        num.getAndIncrement();
    }
}

在讲解volatile的那篇文章中,为了解决多线程环境下i++操作不能同步的情况,使用了原子类AtomicInteger。那么为什么这个类能够解决这个问题呢?它底层是怎么处理的呢?

    /**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

可以看到,它实际上调用了unsafe类的getAndAddInt方法,并给了一个步长1。

在接着看Unsafe

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

在底层实际上采用了自旋锁的思想。如果比较成功就更新值,如果失败就一直重试,直到成功。

三、CAS缺点

  • 循环时间长开销大
  • 只能保证一个共享变量的原子性
  • 带来ABA问题

四、ABA问题

前面两个缺点都很好理解。那么什么是ABA问题捏?

什么是ABA问题

在讲voaltile的时候讲过了JMM内存模型,如果有两个线程同时修改内存中的一个共享变量,A线程修改需要10秒,B线程修改需要2秒,第一次B线程修改结束后,A线程还没有结束,这时B线程又将内存中的值改回来了。导致A线程在10秒后也修改了内存的值。这就是ABA问题。还是来看看代码:

package com.fzkj.juc.cas;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @DESCRIPTION 演示ABA问题
 */
public class ABADemo {

    public static void main(String[] args) {
        aba();
    }

    public static void aba(){
        AtomicInteger num = new AtomicInteger(5);
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            num.compareAndSet(5, 97);
            System.out.println("AAA线程将num的值修改为了 -> " + num);
        }, "AAA").start();

        new Thread(() -> {
            num.compareAndSet(5, 98);
            System.out.println("BBB线程将num的值修改为了 -> " + num);

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 1秒后BBB线程将值修改回来
            num.compareAndSet(98, 5);
            System.out.println("BBB线程将num的值修改为了 -> " + num);
        }, "BBB").start();

        try {
            TimeUnit.SECONDS.sleep(8);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("num 最后的值是 -> " + num);
    }
}

执行结果:

BBB线程将num的值修改为了 -> 98
BBB线程将num的值修改为了 -> 5
AAA线程将num的值修改为了 -> 97
num 最后的值是 -> 97

这就是ABA问题。虽然结果没问题,但是过程却不对。

ABA问题解决

AtomicReference原子引用
package com.fzkj.juc.cas;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @DESCRIPTION 演示ABA问题
 */
public class ABADemo {

    public static void main(String[] args) {
//        aba();
        reference();
    }

    public static void reference(){
        User u1 = new User("1" ,"name1");
        User u2 = new User("2", "name2");

        AtomicReference userReference = new AtomicReference<>();
        userReference.set(u1);

        // 比较并交换
        boolean b = userReference.compareAndSet(u1, u2);
        System.out.println(b + "\t d当前值是 -> " + userReference.get().toString());
        System.out.println(userReference.compareAndSet(u1, u2));
    }


    static class User {
        String id;
        String name;
        public User(String id, String name){
            this.id = id;
            this.name = name;
        }

        @Override
        public String toString() {
            return "User [id = " + id  + ", name = " + name + "]";
        }
    }
}
// 运行结果
true	 d当前值是 -> User [id = 2, name = name2]
false
AtomicStampedReference
package com.fzkj.juc.cas;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @DESCRIPTION 演示ABA问题
 */
public class ABADemo {

    public static void main(String[] args) {
//        aba();
//        reference();
        ABAResolve();
    }

    public static void ABAResolve(){
        AtomicStampedReference<Integer> num = new AtomicStampedReference<>(5, 1);
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(num.compareAndSet(5, 97, 1, 2));
            System.out.println("AAA线程将num的值修改为了 -> " + num.getReference().toString());
        }, "AAA").start();

        new Thread(() -> {
            System.out.println(num.compareAndSet(5, 98, 1, 2));
            System.out.println("BBB线程将num的值修改为了 -> " + num.getReference().toString());
        }, "BBB").start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("num 最后的值是 -> " + num.getReference().toString());
    }
}

运行结果:
true
BBB线程将num的值修改为了 -> 98
false
AAA线程将num的值修改为了 -> 98
num 最后的值是 -> 98

使用AtomicStampedReference类,可以有效的解决ABA带来的问题,在创建这个类的时候会要求传一个期望的stamp,可以理解为版本号,后续会拿版本号进行比较,如果版本号相同才会修改。