CAS机制与自旋锁

CAS(Compare-and-Swap),即比较并替换,java并发包中许多Atomic的类的底层原理都是CAS。
它的功能是判断内存中某个地址的值是否为预期值,如果是就改变成新值,整个过程具有原子性。
具体体现于sun.misc.Unsafe类中的native方法,调用这些native方法,JVM会帮我们实现汇编指令,这些指令是CPU的原子指令,因此具有原子性。
复制代码
public class CASDemo {
    public static void main(String[] args) {
         //初始值5
         AtomicInteger atomicInteger = new AtomicInteger(5);
         //和5比较,设置为10
         System.out.println("预期值:5,当前值:"+atomicInteger);
         System.out.println("是否设置成功:"+atomicInteger.compareAndSet(5, 10));
         //和5比较,设置为15
         System.out.println("预期值:5,当前值:"+atomicInteger);
        System.out.println("是否设置成功:"+atomicInteger.compareAndSet(5, 15));
        System.out.println("当前值:"+atomicInteger);
    }
}
复制代码
输出为:
预期值:5,当前值:5
是否设置成功:true
预期值:5,当前值:10
是否设置成功:false
当前值:10
下面看一下getAndAddInt在底层Unsafe类中的代码(自旋锁),运用到了CAS
//va1为对象,var2为地址值,var4是要增加的值,var5为当前地址中最新的值
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;
}
首先通过volatile的可见性,取出当前地址中的值,作为期望值。如果期望值与实际值不符,就一直循环获取期望值,直到set成功。
适用场景:
  1. CAS 适合简单对象的操作,比如布尔值、整型值等;
  2. CAS 适合冲突较少的情况,如果太多线程在同时自旋,那么长时间循环会导致 CPU 开销很大;
CAS的缺点:
  1. CPU开销过大 : 在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
  2. 不能保证代码块的原子性:CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
  3.  ABA问题:如果内存地址V初次读取的值是A,在CAS等待期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。
ABA问题以及解决:使用带版本号的原子引用AtomicStampedRefence,或者叫时间戳的原子引用,类似于乐观锁。
复制代码
// ABA问题及解决方式
public class ABADemo {

  private static AtomicReference<String> atomicReference = new AtomicReference<>("A");
  private static AtomicStampedReference<String> stampReference = new AtomicStampedReference<>("A",1);

  public static void main(String[] args){
      new Thread(()->{
          //获取到版本号
         int stamp = stampReference.getStamp();
         System.out.println("t1获取到的版本号:"+stamp);
         try {
             //暂停1秒,确保t1,t2版本号相同
             TimeUnit.SECONDS.sleep(1);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         atomicReference.compareAndSet("A","B");
         atomicReference.compareAndSet("B","A");

         stampReference.compareAndSet("A","B",stamp,stamp+1);
         stampReference.compareAndSet("B","A",stamp+1,stamp+2);
         System.out.println("t1线程ABA之后的版本号:"+stampReference.getStamp());

     },"t1").start();

     new Thread(()->{
         //获取到版本号
         int stamp = stampReference.getStamp();
         System.out.println("t2获取到的版本号:"+stamp);
         try {
             //暂停2秒,等待t1执行完成ABA
             TimeUnit.SECONDS.sleep(2);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.print("普通原子类无法解决ABA问题: ");
         System.out.println(atomicReference.compareAndSet("A","C")+"\t"+atomicReference.get());
         System.out.print("版本号的原子类解决ABA问题: ");
         System.out.println(stampReference.compareAndSet("A","C",stamp,stamp+1)+"\t"+stampReference.getReference());

     },"t2").start();
 }
}
复制代码
输出结果:普通原子引用类在另一个线程完成ABA之后继续修改(把A改成了C),带版本号原子引用有效的解决了这个问题。
t1获取到的版本号:1
t2获取到的版本号:1
t1线程ABA之后的版本号:3
普通原子类无法解决ABA问题: true    C
版本号的原子类解决ABA问题: false    A

 

posted @   余额一个亿  阅读(59)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署
点击右上角即可分享
微信分享提示