找回密码
 立即注册
首页 业界区 业界 Java源码分析系列笔记-4.CAS

Java源码分析系列笔记-4.CAS

孙淼淼 2025-6-23 10:03:25
目录

  • 1. 是什么

    • 1.1. 乐观锁与悲观锁
    • 1.2. CAS

  • 2. 如何使用

    • 2.1. Atomic是什么
    • 2.2. Atomic使用
    • 2.3. Atomic原理分析

      • 2.3.1. 构造方法
      • 2.3.2. addAndGet方法
      • 2.3.3. getAndIncrement
      • 2.3.4. decrementAndGet

    • 2.4. AtomicInteger的问题

      • 2.4.1. CPU占用过高
      • 2.4.2. ABA问题

        • 2.4.2.1. 解决方案:版本号

          • 2.4.2.1.1. 原理分析




  • 3. 参考

1. 是什么

要理解CAS,我们首先得了解乐观锁和悲观锁的概念。
1.1. 乐观锁与悲观锁

悲观锁:假设每次操作数据的时候总有人一起操作数据。因此我操作数据前先上锁,直到我操作完释放锁,别人都只能阻塞等待。
乐观锁:假设每次操作数据的时候没人跟我一起操作数据。因此我只在更新的时候检查一下有没有其他人修改了数据,有则重试直到成功。
1.2. CAS

CAS是乐观锁的一种。Java中的AQS、AtomicXXX都是基于CAS实现的。
CAS全称叫compare and set,即比较并设置某个变量的值,他是原子操作。
我们以CAS(A,B)为例,这里涉及了三个值,一个实际内存值A1,当前读取的值A(或者叫预期值A),及其修改值B。当且仅当A1== A时,把值修改为B
2. 如何使用

JUC包中Atomic类的实现都是通过CAS实现的
2.1. Atomic是什么

线程安全的原子类,底层使用CAS实现
2.2. Atomic使用

以AtomicInteger为例
  1. public static void main(String[] args) throws InterruptedException
  2. {
  3.     AtomicInteger val = new AtomicInteger(0);
  4.     Thread addThread = new Thread(()->{
  5.         for (int i = 0; i < 10000; i++)
  6.         {
  7.             val.addAndGet(1);
  8.         }
  9.     });
  10.     Thread decrThread = new Thread(()->{
  11.         for (int i = 0; i < 10000; i++)
  12.         {
  13.             val.decrementAndGet();
  14.         }
  15.     });
  16.     addThread.start();
  17.     decrThread.start();
  18.     addThread.join();
  19.     decrThread.join();
  20.     System.out.println(val.get());//0
  21. }
复制代码
2.3. Atomic原理分析

2.3.1. 构造方法
  1. //使用的是Unsafe.compareAndSwapInt 方法
  2. private static final Unsafe unsafe = Unsafe.getUnsafe();
  3. private static final long valueOffset;
  4. //类加载的时候执行
  5. static {
  6.     try {
  7.             //valueOffset保存的是AtomicInteger value属性在内存中的地址
  8.             //后面调用Unsafe的CAS方法会用到这个值
  9.         valueOffset = unsafe.objectFieldOffset
  10.             (AtomicInteger.class.getDeclaredField("value"));
  11.     } catch (Exception ex) { throw new Error(ex); }
  12. }
  13. //volatile:某线程更新后,其他线程立马看到修改后的值
  14. private volatile int value;
  15. public AtomicInteger(int initialValue) {
  16.     value = initialValue;
  17. }
复制代码
可以看到主要有三个属性:Unsafe unsafe 、long valueOffset和volatile int value

  • 关于Unsafe类的解释参考Unsafe.md,有了这个基础后源码分析就简单多了。
  • valueOffset是value变量在内存中的地址
  • value使用volatile修饰,这样就能保证可见性和有序性
2.3.2. addAndGet方法


  • AtomicInteger.addAndGet
  1. public final int addAndGet(int delta) {
  2.     return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
  3. }
复制代码
调用Unsafe类的getAndAddInt方法对value增加delta
由于Unsafe的方法返回value原值,所以需要加上delta才是增加后的值

  • Unsafe.getAndAddInt
  1. //传入Unsafe.getAndAddInt的参数为(AtomicInteger实例,AtomicInteger value属性的内存地址,增加的值)
  2. public final int getAndAddInt(Object o, long offset, int delta) {
  3.     int v;
  4.     //死循环+cas
  5.     do {
  6.             //获取对象o偏移offset地址的值,即value的值
  7.         v = getIntVolatile(o, offset);
  8.         //判断对象o在偏移offset地址的值 == v(刚刚获取的值)么?是的话把值+delta写入
  9.     } while (!compareAndSwapInt(o, offset, v, v + delta));
  10.     //返回原来的value
  11.     return v;
  12. }
  13. //以下两个都native方法,调用C/C++的方法
  14. public final native boolean compareAndSwapInt(Object o, long offset,
  15.                                                   int expected,
  16.                                                   int x);
  17. public native int getIntVolatile(Object o, long offset);
复制代码
说明都在代码的注释上,不多说了
2.3.3. getAndIncrement


  • AtomicInteger.getAndIncrement
  1. public final int getAndIncrement() {
  2.         //同AtomicInteger.addAndGet方法,调用Unsafe类的getAndAddInt方法对value增加delta,返回value原值
  3.     return unsafe.getAndAddInt(this, valueOffset, 1);
  4. }
复制代码
2.3.4. decrementAndGet


  • AtomicInteger.decrementAndGet
  1. public final int decrementAndGet() {
  2.     //同AtomicInteger.addAndGet方法,调用Unsafe类的getAndAddInt方法对value增加delta,返回value原值
  3.     //只不过传入的delta是个负数,也就是相当于减去了一个数
  4.     return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
  5. }
复制代码
2.4. AtomicInteger的问题

2.4.1. CPU占用过高

多线程并发修改如果竞争特别激烈,那么cpu消耗过大,毕竟是死循环+CAS原子操作修改
2.4.2. ABA问题

假设有两个线程都要修改a的值,ThreadA和ThreadB,操作步骤如下

  • ThreadA:
  1. 第1步 get a为1
  2. 第2步 失去cpu
  3. 第7步 cas(a, 1, 2)
复制代码

  • ThreadB:
  1. 第3步 get a为1
  2. 第4步 cas(a, 1, 3)
  3. 第5步 cas(a, 3, 1)
  4. 第6步 失去cpu
复制代码
从上述顺序看出a的值被线程B从1改为3又改为1, 而线程A以为a的值没有变化,仍然是1,进而把它改为2
2.4.2.1. 解决方案:版本号

我们可以给数据加上版本号来解决ABA问题,即更新的时候不仅比较内存值是否相等,还要比较数据的版本是否相等,只有内存值和版本号相等的情况下才进行更新。
用上面的例子进行说明:

  • ThreadA:
  1. 第1步 get a为(1, 1) //即数据为1,版本为1
  2. 第2步 失去cpu
  3. 第7步 cas(a, 1, 2, 1, 2)//即预期数据为1,要改为2;预期版本号为1,要改为2。这一步执行失败因为此时版本已经为3了,不为1
复制代码

  • ThreadB:
  1. 第3步 get a为(1, 1) //即数据为1,版本为1
  2. 第4步 cas(a, 1, 3, 1, 2)//即预期数据为1,要改为3;预期版本号为1,要改为2
  3. 第5步 cas(a, 3, 1, 2, 3) //即预期数据为3,要改为1;预期版本号为2,要改为3
  4. 第6步 失去cpu
复制代码
Java中已经有一个类实现了版本号:AtomicStampedReference,使用如下:
  1. public static void main(String[] args)
  2. {
  3.     //初始化版本号为0,值为0
  4.     AtomicStampedReference<Integer> val = new AtomicStampedReference<>(0,0);
  5.     //在版本号为0,值为0的基础上cas
  6.     val.compareAndSet(0, 1, 0, 1)
  7. }
复制代码
2.4.2.1.1. 原理分析


  • AtomicStampedReference构造方法
  1. //Pair属性用volatile修饰
  2. private volatile Pair<V> pair;
  3. public AtomicStampedReference(V initialRef, int initialStamp) {
  4.                 //使用初始值和初始版本号构造pair
  5.         pair = Pair.of(initialRef, initialStamp);
  6.     }
  7. //Pair类
  8. private static class Pair<T> {
  9.         //用final修饰,一旦初始化就不能改变,保证了线程安全
  10.     final T reference;
  11.     final int stamp;
  12.     private Pair(T reference, int stamp) {
  13.         this.reference = reference;
  14.         this.stamp = stamp;
  15.     }
  16.     static <T> Pair<T> of(T reference, int stamp) {
  17.         return new Pair<T>(reference, stamp);
  18.     }
  19. }
  20. //类加载的时候初始化Unsafe类和AtomicStampedReference的pair属性的内存地址
  21. private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
  22. private static final long pairOffset =
  23.     objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);
复制代码

  • AtomicStampedReference.compareAndSet
  1. 当前:0,0,修改成:1,1 Param:(0,1,0,1):(期望值,新的值,期望的版本号,新的版本号)
  2. public boolean compareAndSet(V   expectedReference,
  3.                                  V   newReference,
  4.                                  int expectedStamp,
  5.                                  int newStamp) {
  6.         Pair<V> current = pair;
  7.         return
  8.                 //期望值==实际内存值
  9.             expectedReference == current.reference &&
  10.             //期望版本号==实际内存版本号
  11.             expectedStamp == current.stamp &&
  12.             //第1种情况:值和版本号都没有改变,那么不需要做什么
  13.             //1.1 新值==实际内存值
  14.             ((newReference == current.reference &&
  15.               //1.2 新版本号==实际版本号
  16.               newStamp == current.stamp) ||
  17.               
  18.             ||第2中情况:值或版本号有改变,那么cas设置当前pair为新的pair
  19.              casPair(current, Pair.of(newReference, newStamp)));
  20.     }
  21. private boolean casPair(Pair<V> cmp, Pair<V> val) {
  22.         //调用Unsafe类的方法。把pair属性从cmp修改为val
  23.     return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
  24. }
复制代码
3. 参考


  • Java CAS 原理剖析 - 掘金
  • CAS原理分析及ABA问题详解 - 掘金

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册