Skip to content

关于CAS操作的事例是否有问题? #2650

Closed

Description

@zhuqiming

`private volatile int a;
public static void main(String[] args){
CasTest casTest=new CasTest();
new Thread(()->{
for (int i = 1; i < 5; i++) {
casTest.increment(i);
System.out.print(casTest.a+" ");
}
}).start();
new Thread(()->{
for (int i = 5 ; i <10 ; i++) {
casTest.increment(i);
System.out.print(casTest.a+" ");
}
}).start();
}

private void increment(int x){
while (true){
try {
long fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a"));
if (unsafe.compareAndSwapInt(this,fieldOffset,x-1,x))
break;
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}`
文章中给的事例代码,我警告本地运行发现并不符合预期的输出1,2,3,4,5,6,7,8,9;虽然原子操作保证对于a的修改不会出现并发问题,但是两个线程在调用 casTest.increment(i)后的 print方法打印a的时候,会有问题,结果是1,2,3,5,5,6,7,8,9,因为线程2在不符合条件的时候会一直死循环,当第一个线程修改a=4的时候,此时第二个线程会马上修改a=5,这个修改的命令,执行在线程1里的 System.out.print(casTest.a+" "); 这个之前,导致 第一个线程打印a的时候会打印出5,而非4

Activity

  1. LilRind commented on May 7, 2025

    @LilRind

    `private volatile int a; public static void main(String[] args){ CasTest casTest=new CasTest(); new Thread(()->{ for (int i = 1; i < 5; i++) { casTest.increment(i); System.out.print(casTest.a+" "); } }).start(); new Thread(()->{ for (int i = 5 ; i <10 ; i++) { casTest.increment(i); System.out.print(casTest.a+" "); } }).start(); }

    private void increment(int x){ while (true){ try { long fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a")); if (unsafe.compareAndSwapInt(this,fieldOffset,x-1,x)) break; } catch (NoSuchFieldException e) { e.printStackTrace(); } } }` 文章中给的事例代码,我警告本地运行发现并不符合预期的输出1,2,3,4,5,6,7,8,9;虽然原子操作保证对于a的修改不会出现并发问题,但是两个线程在调用 casTest.increment(i)后的 print方法打印a的时候,会有问题,结果是1,2,3,5,5,6,7,8,9,因为线程2在不符合条件的时候会一直死循环,当第一个线程修改a=4的时候,此时第二个线程会马上修改a=5,这个修改的命令,执行在线程1里的 System.out.print(casTest.a+" "); 这个之前,导致 第一个线程打印a的时候会打印出5,而非4

    我认为这个代码的主要问题是在:increment 和打印操作不是原子性的。
    原本代码是想模拟多个线程争抢并修改共享变量的场景。因为线程2在a=4之前是一直循环的,虽然不能修改a,要等到线程1修改a=4。

    代码主要是在打印时机、线程调度上有问题:
    因为increment 和打印操作不是原子性的。
    如果从性能上来说,就是线程执行顺序不确定,会增加CPU消耗:虽然while+CAS本身就是会消耗CPU,但可以优化。

    所以,运行的结果可能就会是这样:
    线程1 依次将 a 更新为 1,2,3,并打印 1,2,3。
    线程1 执行 increment(4),将 a 更新为 4。
    在线程1 打印 a 之前,线程2 执行 increment(5),将 a 更新为 5。
    线程1 打印 a,此时 a=5,输出 5。
    线程2 继续执行 increment(6) 到 increment(9),将 a 更新为 6,7,8,9,并打印 6,7,8,9。
    最终输出:1,2,3,5,5,6,7,8,9。

    可以改进如下:

    import sun.misc.Unsafe;
    import java.lang.reflect.Field;
    
    public class CasTest {
        private volatile int a = 0; // 初始值为 0
        private static final Unsafe unsafe;
        private static final long fieldOffset;
    
        static {
            try {
                // 获取 Unsafe 实例
                Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
                theUnsafe.setAccessible(true);
                unsafe = (Unsafe) theUnsafe.get(null);
                // 获取 a 字段的偏移量
                fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a"));
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    
        public static void main(String[] args) {
            CasTest casTest = new CasTest();
            Thread t1 = new Thread(() -> {
                for (int i = 1; i <= 4; i++) {
                    casTest.increment(i);
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i = 5; i <= 9; i++) {
                    casTest.increment(i);
                }
            });
            t1.start();
            t2.start();
        }
    
        private void increment(int expectedValue) {
            while (true) {
                int current = a; // 读取当前 a 的值
                if (current == expectedValue - 1) { // 检查是否满足递增条件
                    if (unsafe.compareAndSwapInt(this, fieldOffset, current, expectedValue)) {
                        // CAS 成功,打印新值
                        System.out.print(expectedValue + " ");
                        break;
                    }
                }
                // 如果 current 不等于 expectedValue-1,说明其他线程已修改 a,需重试
                // 可选择短暂休眠以减少 CPU 占用(也可以不让出CPU)
                Thread.yield(); // 让出 CPU,优化重试
            }
        }
    }
    

    还有一种方法是用AtomicInteger的compareAndSet(),但是它是包装了的工具类(经过设计过后的),需要理解原理:

    import java.util.concurrent.atomic.AtomicInteger;
    
    public class CasTest {
        private final AtomicInteger a = new AtomicInteger(0);
    
        public static void main(String[] args) {
            CasTest casTest = new CasTest();
            Thread t1 = new Thread(() -> {
                for (int i = 1; i <= 4; i++) {
                    casTest.increment(i);
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i = 5; i <= 9; i++) {
                    casTest.increment(i);
                }
            });
            t1.start();
            t2.start();
        }
    
        private void increment(int expectedValue) {
            while (true) {
                int current = a.get();
                if (current == expectedValue - 1) {
                    if (a.compareAndSet(current, expectedValue)) {
                        System.out.print(expectedValue + " ");
                        break;
                    }
                }
                Thread.yield(); // 优化重试,让出CPU
            }
        }
    }
    
  2. Snailclimb commented on May 8, 2025

    @Snailclimb
    Owner

    private volatile int a; public static void main(String[] args){ CasTest casTest=new CasTest(); new Thread(()->{ for (int i = 1; i < 5; i++) { casTest.increment(i); System.out.print(casTest.a+" "); } }).start(); new Thread(()->{ for (int i = 5 ; i <10 ; i++) { casTest.increment(i); System.out.print(casTest.a+" "); } }).start(); } private void increment(int x){ while (true){ try { long fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a")); if (unsafe.compareAndSwapInt(this,fieldOffset,x-1,x)) break; } catch (NoSuchFieldException e) { e.printStackTrace(); } } } 文章中给的事例代码,我警告本地运行发现并不符合预期的输出1,2,3,4,5,6,7,8,9;虽然原子操作保证对于a的修改不会出现并发问题,但是两个线程在调用 casTest.increment(i)后的 print方法打印a的时候,会有问题,结果是1,2,3,5,5,6,7,8,9,因为线程2在不符合条件的时候会一直死循环,当第一个线程修改a=4的时候,此时第二个线程会马上修改a=5,这个修改的命令,执行在线程1里的 System.out.print(casTest.a+" "); 这个之前,导致 第一个线程打印a的时候会打印出5,而非4

    我认为这个代码的主要问题是在:increment 和打印操作不是原子性的。 原本代码是想模拟多个线程争抢并修改共享变量的场景。因为线程2在a=4之前是一直循环的,虽然不能修改a,要等到线程1修改a=4。

    代码主要是在打印时机、线程调度上有问题: 因为increment 和打印操作不是原子性的。 如果从性能上来说,就是线程执行顺序不确定,会增加CPU消耗:虽然while+CAS本身就是会消耗CPU,但可以优化。

    所以,运行的结果可能就会是这样: 线程1 依次将 a 更新为 1,2,3,并打印 1,2,3。 线程1 执行 increment(4),将 a 更新为 4。 在线程1 打印 a 之前,线程2 执行 increment(5),将 a 更新为 5。 线程1 打印 a,此时 a=5,输出 5。 线程2 继续执行 increment(6) 到 increment(9),将 a 更新为 6,7,8,9,并打印 6,7,8,9。 最终输出:1,2,3,5,5,6,7,8,9。

    可以改进如下:

    import sun.misc.Unsafe;
    import java.lang.reflect.Field;
    
    public class CasTest {
        private volatile int a = 0; // 初始值为 0
        private static final Unsafe unsafe;
        private static final long fieldOffset;
    
        static {
            try {
                // 获取 Unsafe 实例
                Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
                theUnsafe.setAccessible(true);
                unsafe = (Unsafe) theUnsafe.get(null);
                // 获取 a 字段的偏移量
                fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a"));
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    
        public static void main(String[] args) {
            CasTest casTest = new CasTest();
            Thread t1 = new Thread(() -> {
                for (int i = 1; i <= 4; i++) {
                    casTest.increment(i);
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i = 5; i <= 9; i++) {
                    casTest.increment(i);
                }
            });
            t1.start();
            t2.start();
        }
    
        private void increment(int expectedValue) {
            while (true) {
                int current = a; // 读取当前 a 的值
                if (current == expectedValue - 1) { // 检查是否满足递增条件
                    if (unsafe.compareAndSwapInt(this, fieldOffset, current, expectedValue)) {
                        // CAS 成功,打印新值
                        System.out.print(expectedValue + " ");
                        break;
                    }
                }
                // 如果 current 不等于 expectedValue-1,说明其他线程已修改 a,需重试
                // 可选择短暂休眠以减少 CPU 占用(也可以不让出CPU)
                Thread.yield(); // 让出 CPU,优化重试
            }
        }
    }
    

    还有一种方法是用AtomicInteger的compareAndSet(),但是它是包装了的工具类(经过设计过后的),需要理解原理:

    import java.util.concurrent.atomic.AtomicInteger;
    
    public class CasTest {
        private final AtomicInteger a = new AtomicInteger(0);
    
        public static void main(String[] args) {
            CasTest casTest = new CasTest();
            Thread t1 = new Thread(() -> {
                for (int i = 1; i <= 4; i++) {
                    casTest.increment(i);
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i = 5; i <= 9; i++) {
                    casTest.increment(i);
                }
            });
            t1.start();
            t2.start();
        }
    
        private void increment(int expectedValue) {
            while (true) {
                int current = a.get();
                if (current == expectedValue - 1) {
                    if (a.compareAndSet(current, expectedValue)) {
                        System.out.print(expectedValue + " ");
                        break;
                    }
                }
                Thread.yield(); // 优化重试,让出CPU
            }
        }
    }
    

    感谢,很不错的分析👍我已对原文进行修正。

  3. zkj12321 commented on Dec 10, 2025

    @zkj12321

    @Snailclimb 修改后的代码,也就是目前的代码是否存在输出 1 2 3 5 4 6 7 8 9 的可能性,也就是 t1 线程在执行最后一次 casTest.incrementAndPrint(4) 但是还没有来得及输出 4,就被 t2 线程抢占处理机,先输出出来 5,导致上面的情况。如果这种可能有的话,那么4 会有在 5 ~ 9 之后任意一个位置输出的可能性。

  4. smlsix commented on Jul 31, 2026

    @smlsix

    有道理

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions