CAS原理
迪丽瓦拉
2025-06-01 18:35:03
0

文章目录

  • 1、引入CAS
    • 没有CAS之前
    • 使用CAS之后
  • 2、CAS是什么
    • Code
    • 硬件级别保证
    • 底层汇编
    • 自定义原子引用
  • 3、CAS与自旋锁
    • 3.1、是什么
    • 3.2、手写自旋锁
  • 4、ABA问题

1、引入CAS

没有CAS之前

多线程环境下使用synchronized锁保证线程安全 i++(基本数据类型),但是synchronized锁是重量级锁,线程的阻塞和唤醒牵扯到用户态和内核态的切换,效率不高

public class Demo
{volatile int number = 0;//读取public int getNumber(){return number;}//写入加锁保证原子性public synchronized void setNumber(){number++;}
}

使用CAS之后

多线程情况下使用原子类保证线程安全(基本数据类型),类似于乐观锁

public class Demo
{AtomicInteger atomicInteger = new AtomicInteger();public int getAtomicInteger(){return atomicInteger.get();}public void setAtomicInteger(){atomicInteger.getAndIncrement();//先读再加}
}

2、CAS是什么

compare and swap的缩写, 比较并交换, 实现并发算法时常用到的一种技术,它包含三个操作数——内存位置预期原值更新值

  • 执行CAS操作的时候,将内存位置的值与预期原值比较
  • 如果相匹配,那么处理器会自动将该位置值更新为新值
  • 如果不匹配,处理器不做任何操作或者重来(自旋),多个线程同时执行CAS操作只有一个会成功

举个栗子:

线程A读取了值为5,想要更新为6,要将值写回的时候发现线程B和C都进行了操作,已经变成了7,这个时候A不能成功,可能会发生自旋
在这里插入图片描述

Code

public class CASDemo
{public static void main(String[] args) throws InterruptedException{AtomicInteger atomicInteger = new AtomicInteger(5);System.out.println(atomicInteger.compareAndSet(5, 2020)+"\t"+atomicInteger.get());System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t"+atomicInteger.get());}
}
//true 2022
//false 2022

硬件级别保证

对总线加锁,效率比synchronized效率高。

CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。

它是非阻塞且自身原子性,也就是说这玩意效率更高且通过硬件保证,更可靠。

操作系统层面的CAS是一条CPU的原子指令 (cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法 直接通过native方式(封装C++代码)调用了底层的CPU指令cmpxchg

执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果就给总线加锁 ,只有一个线程会对总线加锁成功,加锁成功之后会

执行CAS操作,也就是说CAS的原子性实际上是CPU实现的, 其实在这一点上还是有排他锁的,只是比起用synchronized, 这里的排他

时间要短的多, 所以在多线程情况下性能会比较好

看下源码:

//compareAndSet
//调用了Unsafe类
public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}//compareAndSwapInt
//调用了native方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
//这三个方法是类似的
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

上面三个方法都是类似的,有4个参数
var1:表示要操作的对象
var2:表示要操作对象中属性地址的偏移量
var4:表示需要修改数据的期望的值
var5/var6:表示需要修改为的新值

Unsafe类是什么?

public class AtomicInteger extends Number implements java.io.Serializable {private static final long serialVersionUID = 6214790243416807050L;// setup to use Unsafe.compareAndSwapInt for updatesprivate static final Unsafe unsafe = Unsafe.getUnsafe();static {try {valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); }}private volatile int value;//保证变量修改后多线程之间的可见性}

1、CAS理念 ,落地就是Unsafe类 , 它是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法(C++封装)来访问,基于Unsafe可以直接操作 特定内存 的数据 。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

Unsafe类中的方法都直接调用操作系统底层资源执行相应任务

2、变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的

 public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1);}

3、变量value用volatile修饰

CAS的全称为Compare-And-Swap,它是一条CPU并发原语。

它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

AtomicInteger 类主要利用 CAS (compare and swap) + volatilenative 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令 。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语 ,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

分析下源码:

new AtomicInteger().getAndIncrement();//AtomicInteger.java
public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1);}//Unsafe.class
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;}//Unsafe.class
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

假定一个场景

假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上):

  • AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。

  • 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起

  • 线程B也通过getIntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。

  • 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。

  • 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。
    在这里插入图片描述

底层汇编

CAS是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性

实现方式是基于硬件平台的汇编指令,在intel的CPU中(X86机器上),使用的是汇编指令cmpxchg指令

核心思想就是:比较要更新变量的值V和预期值E(compare),相等才会将V的值设为新值N(swap)如果不相等自旋再来

自定义原子引用

譬如AtomicInteger原子整型,可否有其他原子类型?比如AtomicUser,当然是可以的,将其丢入泛型中Class AtomicReference即可

举个栗子:

@Getter
@ToString
@AllArgsConstructor
class User
{String userName;int    age;
}public class AtomicReferenceDemo
{public static void main(String[] args){User zhang = new User("张三",24);User li = new User("李四",26);//将类型User丢入泛型即可AtomicReference atomicReferenceUser = new AtomicReference<>();atomicReferenceUser.set(zhang);//将这个原子类设置为张三//张三换位李四
9        //true   User(userName=li,age=28)System.out.println(atomicReferenceUser.compareAndSet(zhang,li)+"\t"+atomicReferenceUser.get().toString());//false   User(userName=li,age=28)}
}

3、CAS与自旋锁

3.1、是什么

自旋锁是 指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,

当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

3.2、手写自旋锁

题目:实现一个自旋锁
自旋锁好处:循环比较获取没有类似wait的阻塞。

通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现
当前有线程持有锁,不是null,所以只能通过自旋等待直到A释放锁后B随后抢到

public  class CASDemo {public static class SpinLockDemo {AtomicReference atomicReference = new AtomicReference<>();public void Lock() {Thread thread = Thread.currentThread();System.out.println(Thread.currentThread().getName() + "\t" + "-----come in");while (!atomicReference.compareAndSet(null, thread))//用这个循环实现自旋{}//如果是空的,那我们把thread放进去}public void UnLock() {Thread thread = Thread.currentThread();atomicReference.compareAndSet(thread, null);//把当前线程踢出去,置为nullSystem.out.println(Thread.currentThread().getName() + "\t" + "-------task over,unLock.....");}}public static void main(String[] args){SpinLockDemo spinLockDemo = new SpinLockDemo();new Thread(() -> {spinLockDemo.Lock();try { TimeUnit.SECONDS.sleep( 5 ); } catch (InterruptedException e) { e.printStackTrace(); }spinLockDemo.UnLock();},"A").start();//暂停一会儿线程,保证A线程先于B线程启动并完成try { TimeUnit.MILLISECONDS.sleep( 500); } catch (InterruptedException e) { e.printStackTrace(); }new Thread(() -> {spinLockDemo.Lock();//B  -----come in  B只是尝试去抢锁,但是一直在自旋。spinLockDemo.UnLock();//A结束后 B立马抢到锁,然后马上结束了},"B").start();}
}

在这里插入图片描述

CAS自旋锁虽然不会阻塞,但是如果它一直自旋会占用CPU时间,造成较大的开销。

4、ABA问题

什么是 ABA 问题呢?

CAS算法实现的一个重要前提是将内存中某时刻的值与当前时刻的值进行比较并替换,在这个时间段可能会发生数据的 变化

比如说一个线程 从内存中取出值A ,这时另一个线程也从内存中取出 A ,并且将值修改为了B,然后这个线程偷梁换柱将值又修改为 A ,这时,第一个线程进行CAS操作时发现内存中的值任然是 A ,可以修改成功

虽然第一个线程修改成功了,但这个过程是有问题的呀

这就是所谓的ABA问题

如何解决ABA问题呢?

AtomicStampedReference 版本号 (注意区分前面的Class AtomicReference

Class AtomicStampedReference 相关API

构造方法:

public AtomicStampedReference(V initialRef, int initialStamp) {pair = Pair.of(initialRef, initialStamp);
}

api方法:

public boolean weakCompareAndSet(V   expectedReference,V   newReference,int expectedStamp,int newStamp) {return compareAndSet(expectedReference, newReference,expectedStamp, newStamp);
}

举个栗子具体看看:

public class ABADemo {@NoArgsConstructor@AllArgsConstructor@Datastaticclass Book{private  int id;private String bookName;}//基本情况public static class AtomicStampedDemo1 {public static void main(String[] args) {Book javaBook = new Book(1, "javaBook");AtomicStampedReference stampedReference = new AtomicStampedReference<>(javaBook,1);System.out.println(stampedReference.getReference()+"\t"+stampedReference.getStamp());Book mysqlBook = new Book(2, "mysqlBook");boolean b= stampedReference.compareAndSet(javaBook, mysqlBook, stampedReference.getStamp(), stampedReference.getStamp() + 1);System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());}}/*** 执行结果* ABADemo.Book(id=1, bookName=javaBook)   1* true       demo1.Book(id=2, bookName=mysqlBook)    2* Process finished with exit code 0*///ABA复现(单线程情况下)public static class AtomicStampedDemo2 {public static void main(String[] args) {Book javaBook = new Book(1, "javaBook");AtomicStampedReference stampedReference = new AtomicStampedReference<>(javaBook,1);//线程1拿到book 的值为javaBookSystem.out.println(stampedReference.getReference()+"\t"+stampedReference.getStamp());Book mysqlBook = new Book(2, "mysqlBook");//这时线程2拿到book 的值并且改为mysqlBookboolean b= stampedReference.compareAndSet(javaBook, mysqlBook, stampedReference.getStamp(), stampedReference.getStamp() + 1);System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());//线程2偷梁换柱的又再次将book值改了回去b= stampedReference.compareAndSet(mysqlBook,javaBook, stampedReference.getStamp(), stampedReference.getStamp() + 1);System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());}}/*** 运行结果:* ABADemo.Book(id=1, bookName=javaBook)    1* true    demo1.Book(id=2, bookName=mysqlBook)   2* true    demo1.Book(id=1, bookName=javaBook)      3* 虽然1.3行内容是一样的,但是版本号不一样* 这里就体现了ABA 问题,偷梁换柱*///ABA复现(多线程情况下)public static class ABADemo2{static AtomicInteger atomicInteger = new AtomicInteger(100);static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);public static void main(String[] args){new Thread(() -> {atomicInteger.compareAndSet(100,101);atomicInteger.compareAndSet(101,100);//这里 中间就有人动过了,虽然值是不变的,假如不检查版本号,CAS就直接能成功了},"t1").start();new Thread(() -> {//暂停一会儿线程try { Thread.sleep( 500 ); } catch (InterruptedException e) { e.printStackTrace(); };System.out.println(atomicInteger.compareAndSet(100, 2022)+"\t"+atomicInteger.get());},"t2").start();//暂停一会儿线程,main彻底等待上面的ABA出现演示完成。try { Thread.sleep( 2000 ); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println("============以下是ABA问题的解决=============================");new Thread(() -> {int stamp = atomicStampedReference.getStamp();System.out.println(Thread.currentThread().getName()+"\t 首次版本号:"+stamp);//1-----------初始获得一样的版本号//暂停500毫秒,保证t4线程初始化拿到的版本号和我一样,try { TimeUnit.MILLISECONDS.sleep( 500 ); } catch (InterruptedException e) { e.printStackTrace(); }atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);System.out.println(Thread.currentThread().getName()+"\t 2次版本号:"+atomicStampedReference.getStamp());atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);System.out.println(Thread.currentThread().getName()+"\t 3次版本号:"+atomicStampedReference.getStamp());},"t3").start();new Thread(() -> {int stamp = atomicStampedReference.getStamp();//记录一开始的版本号,并且写死System.out.println(Thread.currentThread().getName()+"\t 首次版本号:"+stamp);//1------------初始获得一样的版本号//暂停1秒钟线程,等待上面的t3线程,发生了ABA问题try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }boolean result = atomicStampedReference.compareAndSet(100,2019,stamp,stamp+1);//这个还是初始的版本号,但是实际上版本号被T3修改了,所以肯定会失败System.out.println(Thread.currentThread().getName()+"\t"+result+"\t"+atomicStampedReference.getReference());},"t4").start();}/*** 运行结果:* true    2022* ============以下是ABA问题的解决=============================* t3   首次版本号:1* t4   首次版本号:1* t3   2次版本号:2* t3   3次版本号:3* t4  false  100** Process finished with exit code 0*/}
}

总之,要避免ABA问题 ,就要比较和版本号一起上

相关内容