JUC——CAS

本文详细介绍了CAS(Compare and Swap)操作在多线程编程中的应用及其存在的ABA问题。通过示例代码展示了ABA问题的产生过程,并提出了AtomicStampedReference作为解决方案。AtomicStampedReference通过版本号来避免ABA问题,确保了在并发环境下的正确性。此外,还讨论了CAS仅能保证单个变量原子操作的限制以及循环带来的开销问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. CAS

在多线程编程时,如果想保证一段代码具有原子性,通过会使用锁来解决,而CAS是通过硬件指令来达到比较并交换的过程;

CAS原理

CAS包括三个值:
 V:内存地址;
 A:期望值;
 B:新值;
 如果这个内存地址V的值和期望值A相等,则将其赋值为B;

2 CAS存在的问题

2.1 ABA问题

在多线程并发场景下:线程A、B、C 同时对资源 R=1进行修改,线程A期望将R修改为2,在修改之前,线程B拿到CPU资源将R修改为3,线程B结束后,线程C拿到CPU资源将R修改为1,此时线程A接着去做修改,成功了,在线程A看来,R=1一直没有被修改,所以造成ABA问题;

staticFieldOffset 方法用于获取静态属性 Field 在 Class 对象中的偏移量,在 CAS 操作静态属性时,会用到这个偏移量。
objectFieldOffset 方法用于获取非静态 Field (非静态属性)在 Object 实例中的偏移量,在 CAS 操作对象的非静态属性时,会用到这个偏移量。问题复现:*

public class CASTest {
    private static Unsafe unsafe;
    private static long valueOffset;
    static {
        Field theUnsafe = null;
        try {
            theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);

            valueOffset = unsafe.objectFieldOffset(CASTest.class.getDeclaredField("value"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private volatile int value=1;

    public static void main(String[] args) throws Exception{
        final CASTest casTest = new CASTest();
        casTest.testABA(casTest);
    }
    private void testABA(final CASTest casTest){
        final Thread threadB = new Thread(new Runnable() {
            public void run() {
                System.out.println("开始前线程B:"+casTest.value);
                int expect = 1;
                int update = 3;
                System.out.println("准备更新时线程B:"+casTest.value);
                System.out.println("线程B:"+unsafe.compareAndSwapInt(casTest, valueOffset, expect, update));
            }
        });
        final Thread threadC = new Thread(new Runnable() {
            public void run() {
                System.out.println("开始前线程C:"+casTest.value);
                try {
                    threadB.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int expect = 3;
                int update = 1;
                System.out.println("准备更新时线程C:"+casTest.value);
                System.out.println("线程C:"+unsafe.compareAndSwapInt(casTest, valueOffset, expect, update));
            }
        });
        final Thread threadA = new Thread(new Runnable() {
            public void run() {
                System.out.println("开始前线程A:"+casTest.value);
                try {
                    threadC.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int expect = 1;
                int update = 2;
                System.out.println("准备更新时线程A:"+casTest.value);
                System.out.println("线程A:"+unsafe.compareAndSwapInt(casTest, valueOffset, expect, update));
            }
        });
        threadA.start();
        threadC.start();
        threadB.start();
    }
}
输出结果:
开始前线程A1
开始前线程C1
开始前线程B1
准备更新时线程B1
线程B:true
准备更新时线程C3
线程C:true
准备更新时线程A1
线程A:true

通过控制线程执行顺序,复现此问题,但是我们发现如果是对于数值类型的ABA问题不是问题;

来试一下compareAndSwapObject,同样让它产生ABA问题,看是否有影响:

public class CASTest {
    private static Unsafe unsafe;
    private static long valueOffset;
    static {
        Field theUnsafe = null;
        try {
            theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);

            valueOffset = unsafe.objectFieldOffset(CASTest.class.getDeclaredField("value"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private volatile Student value= new Student("");

    public static void main(String[] args) throws Exception{
        final CASTest casTest = new CASTest();
        casTest.testABA(casTest);
    }
    private void testABA(final CASTest casTest){
        final Thread threadB = new Thread(new Runnable() {
            public void run() {
                System.out.println("开始前线程B:"+casTest.value);
                System.out.println("准备更新时线程B:"+casTest.value);
                System.out.println("线程B:"+
                        unsafe.compareAndSwapObject(casTest,
                                valueOffset,
                                casTest.value,
                                new Student("tom")));
            }
        });
        final Thread threadC = new Thread(new Runnable() {
            public void run() {
                System.out.println("开始前线程C:"+casTest.value);
                Student expect = casTest.value;
                try {
                    threadB.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("准备更新时线程C:"+casTest.value);
                System.out.println("线程C:"+unsafe.compareAndSwapObject(
                        casTest,
                        valueOffset,
                        casTest.value,
                        new Student("")));
            }
        });
        final Thread threadA = new Thread(new Runnable() {
            public void run() {
                System.out.println("开始前线程A:"+casTest.value);
                try {
                    threadC.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("准备更新时线程A:"+casTest.value);
                System.out.println("线程A:"+unsafe.compareAndSwapObject(
                        casTest,
                        valueOffset,
                        casTest.value,
                        new Student("ThreadA")));
            }
        });
        threadA.start();
        threadC.start();
        threadB.start();
    }
}

class Student{
    private String name;
    public Student(String name){
        this.name=name;
    }
}
开始前线程Acom.didichuxing.erp.srmsync.juc.Student@6fe706ec
开始前线程Ccom.didichuxing.erp.srmsync.juc.Student@6fe706ec
开始前线程Bcom.didichuxing.erp.srmsync.juc.Student@6fe706ec
准备更新时线程Bcom.didichuxing.erp.srmsync.juc.Student@6fe706ec
线程B:true
准备更新时线程Ccom.didichuxing.erp.srmsync.juc.Student@40d974e1
线程C:true
准备更新时线程Acom.didichuxing.erp.srmsync.juc.Student@6120f560
线程A:true

可以看到Student实例已经发生了多次变化,volatile Student value属于多线程内存可见,当其他线程对value做了修改,另外的线程是能获取到最新值的,去做compareAndSwapObject是成功的,但如果这样:;

final Thread threadA = new Thread(new Runnable() {
    public void run() {
        System.out.println("开始前线程A:"+casTest.value);
        Student student = casTest.value;
        try {
            threadC.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("准备更新时线程A:"+casTest.value);
        System.out.println("线程A:"+unsafe.compareAndSwapObject(
                casTest,
                valueOffset,
                student,
                new Student("ThreadA")));
    }
});

结果肯定是false,那既然是false,引用类型会产生ABA问题吗?个人理解,我觉得引用类型不会产生ABA问题,但如果另外一个线程对value的age字段做了修改,会不会产生问题,我们试一下:

final Thread threadC = new Thread(new Runnable() {
    public void run() {
        casTest.value.age=1;

    }
});
final Thread threadA = new Thread(new Runnable() {
    public void run() {
        System.out.println("开始前线程A:"+casTest.value);
        Student student = casTest.value;
        try {
            threadC.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("准备更新时线程A:"+casTest.value);
        System.out.println("线程A:"+unsafe.compareAndSwapObject(
                casTest,
                valueOffset,
                student,
                new Student("ThreadA")));
    }
});
开始前线程A{name:,age:0}
准备更新时线程A{name:,age:1}
线程A:true

这倒是产生了另外一个问题,如果预期和内存中的student是同一个地址,但是成员变量已经产生了变化,同样更新成功了;

2.2 ABA问题解决办法

总结一下上面提到的ABA问题:

  1. 数值类型ABA问题;
  2. 引用类型对象成员变量被改变时,与内存中的不一致,任然能够修改成功;

ABA问题解决:

Integer initValue = 100;
Integer initVersion = 1;
final AtomicStampedReference<Integer> reference = new AtomicStampedReference<Integer>(initValue,initVersion);

final Thread threadA = new Thread(new Runnable() {
    public void run() {
        int stamp = reference.getStamp();
        System.out.println("线程A:" +
                reference.compareAndSet(100, 200, stamp, stamp + 1));
    }
});

final Thread threadB = new Thread(new Runnable() {
    public void run() {
        int stamp = reference.getStamp();
        try {
            threadA.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程B:" +
                reference.compareAndSet(200, 300, stamp, stamp + 1));
    }
});
threadB.start();
threadA.start();

AtomicStampedReference 原理:

public class AtomicStampedReference<V> {

    // 定义引用类型,包装值和版本号;
    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;

    // 比较并交换
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            // 先做一次校验,如果在这里都已经不一致,则直接返回false,这里没有加锁,那么它可能会存在并发;
            // 可能会有两个线程同时进来,判断并且都成立,则两个线程都会进入到:casPair方法;
            // Pair<V> current = pair; 多个线程进入到compareAndSet方法时,都已经保留了当前的pair值,那如果pair被其他线程修改,则另外一个线程去做cas的时候一定会返回false,所以这块是通过这种方式来防止并发的;
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

    private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
    private static final long pairOffset =
        objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);

    private boolean casPair(Pair<V> cmp, Pair<V> val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }

    static long objectFieldOffset(sun.misc.Unsafe UNSAFE,
                                  String field, Class<?> klazz) {
        try {
            return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field));
        } catch (NoSuchFieldException e) {
            // Convert Exception to corresponding Error
            NoSuchFieldError error = new NoSuchFieldError(field);
            error.initCause(e);
            throw error;
        }
    }
}

2.3 只能保证一个共享变量的原子操作

至于这个问题,可以参考:AtomicStampedReference 将多个变量封装成对象,再对对象做CAS;

2.4 循环时间长开销大

关于这个问题,我觉得应该还好吧,可以根据服务器配置查询MIPS指标,CPU每秒执行指令数是远远超过CAS的并发;

### Java 并发包 (JUC) 的详细介绍与使用方法 #### 什么是 JUCJava 并发包(`java.util.concurrent`, 简称 JUC)是 JDK 提供的一套用于支持多线程并发编程的工具集。它不仅简化了开发人员编写复杂并发程序的工作,还提供了许多高级功能来优化性能和安全性。 --- #### 主要组成部分及其详解 ##### 1. **原子变量** 原子变量通过硬件级别的 CAS(Compare And Swap)机制实现了无锁化的高效操作[^2]。以下是常见的原子类: - `AtomicInteger`: 支持整型的原子更新。 - `AtomicLong`: 支持长整型的原子更新。 - `AtomicReference<T>`: 支持对象类型的原子更新。 示例代码如下: ```java import java.util.concurrent.atomic.AtomicInteger; public class AtomicExample { private static AtomicInteger counter = new AtomicInteger(0); public static void main(String[] args) { for(int i=0;i<1000;i++) { new Thread(() -> { int value = counter.incrementAndGet(); System.out.println(Thread.currentThread().getName() + ": " + value); }).start(); } } } ``` --- ##### 2. **线程池** 线程池的核心类是 `ThreadPoolExecutor`,其设计目标是在高负载下减少全局锁的影响并提高吞吐量[^3]。创建线程的方式主要包括继承 `Thread` 类、实现 `Runnable` 接口以及实现带有返回值的 `Callable` 接口。 推荐使用的工厂模式来自 `Executors` 工具类,例如固定大小线程池: ```java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExample { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(5); for(int i=0;i<10;i++) { final int taskNumber = i; executor.submit(() -> { System.out.println("Task "+taskNumber+" executed by "+Thread.currentThread().getName()); }); } executor.shutdown(); } } ``` --- ##### 3. **同步工具** JUC 中提供了一些高级同步工具,帮助开发者解决复杂的线程协作问题: ###### a. **CountDownLatch** `CountDownLatch` 可以让一个或多个线程等待其他线程完成某些操作后再继续执行[^5]。 ```java import java.util.concurrent.CountDownLatch; public class CountDownLatchExample { public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(3); for(int i=0;i<3;i++) { new Thread(() -> { try { System.out.println(Thread.currentThread().getName()+" is working..."); Thread.sleep((long)(Math.random()*1000)); } catch (InterruptedException e) {} latch.countDown(); }).start(); } latch.await(); System.out.println("All tasks are done."); } } ``` ###### b. **CyclicBarrier** 类似于 `CountDownLatch`,但它可以重复使用。 ```java import java.util.concurrent.BARRIER; public class CyclicBarrierExample { BARRIER barrier = new BARRIER(3, () -> System.out.println("All parties reached the barrier.")); public void testBarrier() { for(int i=0;i<3;i++) { new Thread(() -> { try { System.out.println(Thread.currentThread().getName()+": Waiting at barrier"); barrier.await(); } catch(Exception ex) {} System.out.println(Thread.currentThread().getName()+": Continuing work after barrier"); }).start(); } } } ``` ###### c. **Semaphore** `Semaphore` 控制同时访问某一资源的最大线程数。 ```java import java.util.concurrent.Semaphore; public class SemaphoreExample { private Semaphore semaphore = new Semaphore(3); public void accessResource() { try { semaphore.acquire(); System.out.println(Thread.currentThread().getName()+" accessing resource..."); Thread.sleep(1000); } catch (Exception e) {} finally { semaphore.release(); } } } ``` --- ##### 4. **锁机制** 除了传统的 `synchronized` 关键字外,JUC 还引入了更加灵活的锁——`ReentrantLock` 和条件队列 `Condition`[^4]。 示例代码展示如何使用显式锁: ```java import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockExample { private final Lock lock = new ReentrantLock(); public void criticalSection() { lock.lock(); try { // Critical section code here... System.out.println(Thread.currentThread().getName()+" entered critical section."); } finally { lock.unlock(); } } } ``` --- ##### 5. **Fork/Join 框架** 适用于大规模数据处理场景中的分治算法[^1]。 ```java import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveTask; class SumTask extends RecursiveTask<Integer> { private final int[] array; private final int start; private final int end; public SumTask(int[] array, int start, int end) { this.array = array; this.start = start; this.end = end; } @Override protected Integer compute() { if(end - start <= 10){ int sum = 0; for(int i=start;i<=end;i++) sum += array[i]; return sum; }else{ int mid = (start + end)/2; SumTask leftTask = new SumTask(array,start,mid); SumTask rightTask = new SumTask(array,mid+1,end); invokeAll(leftTask,rightTask); return leftTask.join() + rightTask.join(); } } } public class ForkJoinExample { public static void main(String[] args) { int[] data = new int[100]; // Initialize with values ForkJoinPool pool = new ForkJoinPool(); int result = pool.invoke(new SumTask(data,0,data.length-1)); System.out.println(result); } } ``` --- #### 最佳实践建议 1. 尽可能复用线程而不是频繁创建销毁它们。 2. 避免长时间持有锁以防死锁发生。 3. 对共享状态的操作优先考虑不可变性或者借助原子变量代替手动加锁。 4. 调试并发程序时注意观察竞争条件、内存可见性和活锁等问题。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值