【JavaEE】CAS(Compare And Swap)操作

文章目录

  • 什么是 CAS
  • CAS 的应用
  • 如何使用 CAS 操作实现自旋锁
  • CAS 的 ABA 问题
  • CAS 相关面试题

什么是 CAS

CAS(Compare and Swap)是一种原子操作,用于在无锁情况下保证数据一致性的问题。它包含三个操作数——内存位置、预期原值及更新值。在执行CAS操作时,会将内存位置的值与预期原值进行比较。如果两者相等,则处理器会自动将该位置的值更新为新值;如果不相等,则处理器不做任何操作。这个过程是原子的,即在整个操作期间,不会被其他线程或进程中断。

在多线程并发编程中,CAS操作可以避免传统的锁机制引起的线程阻塞和上下文切换等问题,提高程序的并发性能。

CAS伪代码

boolean CAS(address, expectValue, swapValue) {
	//如果内存address中的值和expectValue相等话,
	//就将swapValue的值赋给adress,并且返回true
	if (&address == expectValue) {
		&address = swapValue;
		return true;
	}
	return false;
}

CAS 是一个 CPU 指令,具有原子性,而具有原子性的操作就代表着不需要加锁就可以保证线程的安全,所以 CAS 操作就可以替代某些加锁的操作。

CAS 本质上是 CPU 提供的指令,然后被操作系统封装形成 API 后,又被 JVM 或者其它封装成为 API 之后,我们程序员才可以直接使用 CAS 的相关操作。

CAS 的应用

CAS 经过 CPU 和 JVM 封装之后,我们在 Java 代码中就可以直接使用 CAS 操作,那么我们来看看在 Java 中如何使用 CAS 操作。

标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的.

【JavaEE】CAS(Compare And Swap)操作_第1张图片

我们可以根据需要创建出合适的类,如果你要进行 CAS 的数据类型为 int 类型的话,就创建 AtomicInteger 类,如果是 boolean 类型的话,就创建出 AtomicBoolean 类型。

【JavaEE】CAS(Compare And Swap)操作_第2张图片

AtomicInteger 类中有很多方法,但是我们今天主要了解 getAndDecrement 方法和 getAndIncrement 方法,它们分别表示–和++操作。

public class Test {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 1000; i++) {
                atomicInteger.getAndIncrement();
            }
        });

        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 1000; i++) {
                atomicInteger.getAndIncrement();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(atomicInteger.get());  //2000
    }
}
public class Test {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger(10000);
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 1000; i++) {
                atomicInteger.getAndDecrement();
            }
        });

        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 1000; i++) {
                atomicInteger.getAndDecrement();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(atomicInteger.get());  //8000
    }
}

++或者–的操作不具有原子性,如果在多线程中进行++或者–操作的时候往往会发生线程不安全问题,导致最终的结果不是我们想要的结果,而这里我们使用 CAS 操作的话就保证了++和–操作的原子性,并且也避免了加锁阻塞的现象,既保证了答案的正确性,又保证了运行速度。

【JavaEE】CAS(Compare And Swap)操作_第3张图片
查看 getAndIncrement 方法我们可以看到这个方法里面又调用了 getAndAddInt 方法,但是这个方法是属于 unsafe 的,unsafe 中的方法都是偏底层且操作较危险的操作。

【JavaEE】CAS(Compare And Swap)操作_第4张图片

可以看到 getAndAddInt 方法中是没有加锁操作的。

在这里插入图片描述
compareAndSwapInt 方法是 native 修饰的本地方法,这个方法是 JVM 底层由 C/C++ 写的,我们是看不到的。

这里 getAndIncremnet 方法还是用伪代码来实现一遍。

class AtomicInteger {
	private int value;
	
	public int getAndIncrement() {
	//这里现在寄存器当中存储value的值
		int oldValue = value;
		//比较内存中的value值是否和寄存器当中的oldValue相同
		//如果相同,则说明该过程中value的值没有被修改,然后将后面的修改值赋给value
		//如果不相同,说明在这个过程中value的值被修改了,那么更新oldValue的值
		while (CAS(value, oldValue, oldValue + 1) != true)
			oldValue = value;
		}
		return oldValue;
	}
}

如何使用 CAS 操作实现自旋锁

前面【JavaEE】锁策略中为大家讲解了什么是自旋锁,自旋锁就是当线程想要获取到锁,但是这个锁正别其他线程使用的时候,一般请情况下线程会进入阻塞等待状态,但是自旋锁不是,它不释放 CPU 资源,反复确认这个锁是否被释放,使得整个操作一直处于用户态操作,减少了内核态操作而增加一些其他操作。接下来,我们就来使用 CAS 操作来实现自旋锁。

public class SpinLock {
	//owner表示当前锁是被哪个线程所拥有,当owner为null的时候表示该所可以被获取
    private Thread owner = null;

	//while判断当前owner时候为null,如果是,则获取到整个锁,修改owner为当前线程
	//如果owner不为null,则表示锁被其他线程使用,那么就会返回false,while里面的
	//判断就为true,进入死循环,直到其他线程使用unlock方法释放锁
    public void lock() {
        while (!CAS(this.owner, null, Thread.currentThread())) {
            
        }
    }
    
    public void unlock() {
        this.owner = null;
    }
}

通过 CAS 操作就解决了当多个线程竞争一个锁的时候,线程进入阻塞等待状态由用户态操作转为内核态的情况,保证了程序处于用户态的操作状态。

CAS 的 ABA 问题

CAS 操作是判断内存中的数据是否和寄存器中的值相等,那么是否会发生一种情况就是:在这个过程中内存中的数据由 A -> B -> A,也就是说内存中的数据被修改了一次,但是最后又被改回来了的情况呢?当然是可能的,那么如果发生这种情况的时候是否会出现问题呢?

使用 CAS 操作的时候,如果发生 ABA 的问题时,一般不会出现问题,但是有些特殊的情况会造成问题。比如:我现在是大学生,每个月我的父母就会向我的银行卡里面打钱,我呢手机绑定了银行卡,就需要从银行卡中将这些钱充值到微信或者支付宝上,我打算充值1000块,但是当我点击充值按钮的时候,因为网卡,我点了一次没反应,所以我又点了一次,当网络好了的时候,它后台就显示我点击了两次,但是实际上我只想充值一次,那么微信或者支付宝的后台就会有两个线程执行 CAS 操作。

【JavaEE】CAS(Compare And Swap)操作_第5张图片

但是如果在这个时候,我的父母又给我银行卡里面打了1000块钱的时候会发生什么呢?

【JavaEE】CAS(Compare And Swap)操作_第6张图片
在这里插入图片描述
那么如何解决 CAS 的 ABA 问题呢?造成 ABA 的问题就是变量既有增加也有减少,如果我们使用的变量是只增或者只减的话,那么就不会发生这种 ABA 问题。我们可以引入一个额外的变量:版本号,换个版本号是只增的,修改一次余额就增加版本号一次,当执行 CAS 操作的时候会判断内存中的版本号和寄存器当中的版本号是否相同,相同则可以执行,不相同就说明中间穿插了其他的修改操作,不执行修改操作。

public class Test2 {
    private int value;
	//number表示版本号
    private int number = 0;

    public void add(int money) {
        int oldNumber = number;
        //进行一次修改操作之后版本号就加1
        if (CAS(number, oldNumber, oldNumber + 1)) {
            value += money;
        }
    }
}

CAS 相关面试题

1) 讲解下你自己理解的 CAS 机制

全称 Compare and swap, 即 “比较并交换”. 相当于通过一个原子的操作, 同时完成 “读取内存, 比较是否相等, 修改内存” 这三个步骤. 本质上需要 CPU 指令的支撑

2) ABA问题怎么解决?

给要修改的数据引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增; 如果发现当前版本号比之前读到的版本号大, 就认为操作失败

你可能感兴趣的:(JavaEE,java-ee,java,CAS)