JavaGuide icon indicating copy to clipboard operation
JavaGuide copied to clipboard

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

Open zhuqiming opened this issue 8 months ago • 2 comments

`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

zhuqiming avatar Mar 23 '25 08:03 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

我认为这个代码的主要问题是在: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
        }
    }
}

LilRind avatar May 07 '25 03:05 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
        }
    }
}

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

Snailclimb avatar May 08 '25 06:05 Snailclimb