并发编程利器 Java CAS 原子类全解

开发 后端
本文将全面解析 Java 中的 CAS 原子类,探讨其背后的原理、应用场景以及如何有效利用这些工具来提升程序的并发性能和安全性。

在现代软件开发中,多线程编程已经成为构建高性能、高响应性应用程序的关键技术之一。然而,多线程环境下的数据一致性问题一直是开发者面临的一大挑战。为了解决这一难题,Java 平台提供了多种并发控制工具和机制,其中 java.util.concurrent.atomic 包中的 CAS(Compare-and-Swap)原子类尤为突出。 

本文将全面解析 Java 中的 CAS 原子类,探讨其背后的原理、应用场景以及如何有效利用这些工具来提升程序的并发性能和安全性。通过深入理解 CAS 机制,读者将能够更好地应对复杂的并发编程场景,编写出更加健壮和高效的代码。

一、什么是CAS

CAS全称Compare-And-Swap,是一种无锁编程算法,即比较当前的值与旧值是否相等若相等则进行修改操作(乐观锁机制),该类常用于多线程共享变量的修改操作。而其底层实现也是基于硬件平台的汇编指令,JVM只是封装其调用仅此而已。而本文会基于以下大纲展开对CAS的探讨。

二、CAS基础使用示例

如下所示,可以看出使用封装CAS操作的AtomicInteger操作多线程共享变量无需我们手动加锁,因为避免过多人为操作这就大大减少了多线程操作下的失误。

使用原子类操作共享数据:

public class CasTest {
    private AtomicInteger count = new AtomicInteger();


    public void increment() {
        count.incrementAndGet();
    }
    // 使用 AtomicInteger 后,不需要加锁,也可以实现线程安全
    public int getCount() {
        return count.get();
    }

    public static void main(String[] args) {

    }
}

使用sync锁操作数据:

public class Test {
    private int i=0;
    public synchronized int add(){
        return i++;
    }
}

三、从源码角度了解java如何封装汇编的UNSAFE

代码也很简单,就是拿到具有可见性的volatile变量i,然后判断i和当前对象paramObject对应的i值是否一致,若一致则说明没被人该过,进而进行修改操作,反之自旋循环获取在进行CAS。

public final int getAndAddInt(Object paramObject, long paramLong, int paramInt)
  {
    int i;
    do
      i = getIntVolatile(paramObject, paramLong);
    while (!compareAndSwapInt(paramObject, paramLong, i, i + paramInt));
    return i;
  }

  public final long getAndAddLong(Object paramObject, long paramLong1, long paramLong2)
  {
    long l;
    do
      l = getLongVolatile(paramObject, paramLong1);
    while (!compareAndSwapLong(paramObject, paramLong1, l, l + paramLong2));
    return l;
  }

  public final int getAndSetInt(Object paramObject, long paramLong, int paramInt)
  {
    int i;
    do
      i = getIntVolatile(paramObject, paramLong);
    while (!compareAndSwapInt(paramObject, paramLong, i, paramInt));
    return i;
  }

  public final long getAndSetLong(Object paramObject, long paramLong1, long paramLong2)
  {
    long l;
    do
      l = getLongVolatile(paramObject, paramLong1);
    while (!compareAndSwapLong(paramObject, paramLong1, l, paramLong2));
    return l;
  }

  public final Object getAndSetObject(Object paramObject1, long paramLong, Object paramObject2)
  {
    Object localObject;
    do
      localObject = getObjectVolatile(paramObject1, paramLong);
    while (!compareAndSwapObject(paramObject1, paramLong, localObject, paramObject2));
    return localObject;
  }

四、手写Unsafe实现20个线程500次CAS自增

代码逻辑和注释如下,读者可自行debug查看逻辑:

public class CasCountInc {

    private static Logger logger = LoggerFactory.getLogger(CasCountInc.class);

    // 获取Unsafe对象
    private static Unsafe unsafe = getUnsafe();

    // 线程池数目
    private static final int THREAD_COUNT = 20;

    // 每个线程运行自增次数
    private static final int EVERY_THREAD_ADD_COUNT = 500;

    // 自增的count的值,volatile保证可见性
    private volatile int count = 0;

    // count字段的偏移量
    private static long countOffSet;

    private static Unsafe getUnsafe() {
        Unsafe unsafe = null;
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            logger.info("获取unsafe失败,失败原因:[{}]", e.getMessage(), e);
        }
        return unsafe;
    }


    static {
        try {
            countOffSet = unsafe.objectFieldOffset(CasCountInc.class.getDeclaredField("count"));
        } catch (NoSuchFieldException e) {
            logger.error("获取count的偏移量报错,错误原因:[{}]", e.getMessage(), e);
        }
    }


    public void inc() {
        int oldCount = 0;
        //基于cas完成自增
        do {
            oldCount = count;
        } while (!unsafe.compareAndSwapInt(this, countOffSet, oldCount, oldCount + 1));
    }


    public static void main(String[] args) throws InterruptedException {
        CasCountInc casCountInc = new CasCountInc();
        CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
        IntStream.range(0, THREAD_COUNT).forEach(i -> {
            new Thread(() -> {
                IntStream.range(0, EVERY_THREAD_ADD_COUNT).forEach((j) -> {
                    casCountInc.inc();
                });
                countDownLatch.countDown();
            }).start();
        });

        countDownLatch.await();

        logger.info("count最终结果为 [{}]", casCountInc.count);
    }
}

五、原子类简介

1.原子类更新基本类型

原子类基本类型的格式为Atomic+包装类名,这里笔者列举几个比较常用的:

  • AtomicBoolean: 原子更新布尔类型。
  • AtomicInteger: 原子更新整型。
  • AtomicLong: 原子更新长整型。

2.原子类更新数组类型

  • AtomicIntegerArray: 原子更新整型数组里的元素。
  • AtomicLongArray: 原子更新长整型数组里的元素。
  • AtomicReferenceArray: 原子更新引用类型数组里的元素。

对应我们给出AtomicIntegerArray原子操作数组的示例:

public class AtomicIntegerArrayDemo {


    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerArray array = new AtomicIntegerArray(new int[] { 0, 0 });
        System.out.println(array);
//        索引1位置+2
        System.out.println(array.getAndAdd(1, 2));
        System.out.println(array);
    }
}

3.原子类更新引用类型

  • AtomicReference: 原子更新引用类型。
  • AtomicStampedReference: 原子更新引用类型, 内部使用Pair来存储元素值及其版本号。
  • AtomicMarkableReferce: 原子更新带有标记位的引用类型。

对应的我们给出原子操作引用类型的代码示例:

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceTest {

    public static void main(String[] args){

        // 创建两个Person对象,它们的id分别是101和102。
        Person p1 = new Person(101);
        Person p2 = new Person(102);
        // 新建AtomicReference对象,初始化它的值为p1对象
        AtomicReference ar = new AtomicReference(p1);
        // 通过CAS设置ar。如果ar的值为p1的话,则将其设置为p2。
        ar.compareAndSet(p1, p2);

        Person p3 = (Person)ar.get();
        System.out.println("p3 is "+p3);
        System.out.println("p3.equals(p1)="+p3.equals(p1));
        System.out.println("p3.equals(p2)="+p3.equals(p2));
    }
}



class Person {
    volatile long id;
    public Person(long id) {
        this.id = id;
    }
    public String toString() {
        return "id:"+id;
    }
}

六、原子类更新成员变量

通过原子类型操作成员变量大体有以下几个更新器:

  • AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
  • AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
  • AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型
  • AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述。

如下所示,我们创建一个基础类DataDemo,通过原子类CAS操作字段值进行自增操作。

public class TestAtomicIntegerFieldUpdater {


    private static Logger logger = LoggerFactory.getLogger(TestAtomicIntegerFieldUpdater.class);

    public static void main(String[] args) {
        TestAtomicIntegerFieldUpdater tIA = new TestAtomicIntegerFieldUpdater();
        tIA.doIt();
    }

    /**
     * 返回需要更新的整型字段更新器
     *
     * @param fieldName
     * @return
     */
    public AtomicIntegerFieldUpdater<DataDemo> updater(String fieldName) {
        return AtomicIntegerFieldUpdater.newUpdater(DataDemo.class, fieldName);
    }

    public void doIt() {
        DataDemo data = new DataDemo();
        // 修改公共变量,返回更新前的旧值 0
        AtomicIntegerFieldUpdater<DataDemo> updater = updater("publicVar");
        int oldVal = updater.getAndIncrement(data);
        logger.info("publicVar 更新前的值[{}] 更新后的值 [{}]", oldVal, data.publicVar);


        // 更新保护级别的变量
        AtomicIntegerFieldUpdater<DataDemo> protectedVarUpdater = updater("protectedVar");
        int oldProtectedVar = protectedVarUpdater.getAndAdd(data, 2);
        logger.info("protectedVar 更新前的值[{}] 更新后的值 [{}]", oldProtectedVar, data.protectedVar);


        // logger.info("privateVar = "+updater("privateVar").getAndAdd(data,2)); 私有变量会报错

        /*
         * 下面报异常:must be integer
         * */
//        logger.info("integerVar = "+updater("integerVar").getAndIncrement(data));
        //logger.info("longVar = "+updater("longVar").getAndIncrement(data));
    }


    class DataDemo {

        // 公共且可见的publicVar
        public volatile int publicVar = 0;
        // 保护级别的protectedVar
        protected volatile int protectedVar = 4;
        // 私有变量
        private volatile int privateVar = 5;

        // final 不可变量
        public final int finalVar = 11;

        public volatile Integer integerVar = 19;
        public volatile Long longVar = 18L;

    }

}
public class TestAtomicIntegerFieldUpdater {


    private static Logger logger = LoggerFactory.getLogger(TestAtomicIntegerFieldUpdater.class);

    public static void main(String[] args) {
        TestAtomicIntegerFieldUpdater tIA = new TestAtomicIntegerFieldUpdater();
        tIA.doIt();
    }

    /**
     * 返回需要更新的整型字段更新器
     *
     * @param fieldName
     * @return
     */
    public AtomicIntegerFieldUpdater<DataDemo> updater(String fieldName) {
        return AtomicIntegerFieldUpdater.newUpdater(DataDemo.class, fieldName);
    }

    public void doIt() {
        DataDemo data = new DataDemo();
        // 修改公共变量,返回更新前的旧值 0
        AtomicIntegerFieldUpdater<DataDemo> updater = updater("publicVar");
        int oldVal = updater.getAndIncrement(data);
        logger.info("publicVar 更新前的值[{}] 更新后的值 [{}]", oldVal, data.publicVar);


        // 更新保护级别的变量
        AtomicIntegerFieldUpdater<DataDemo> protectedVarUpdater = updater("protectedVar");
        int oldProtectedVar = protectedVarUpdater.getAndAdd(data, 2);
        logger.info("protectedVar 更新前的值[{}] 更新后的值 [{}]", oldProtectedVar, data.protectedVar);


        // logger.info("privateVar = "+updater("privateVar").getAndAdd(data,2)); 私有变量会报错

        /*
         * 下面报异常:must be integer
         * */
//        logger.info("integerVar = "+updater("integerVar").getAndIncrement(data));
        //logger.info("longVar = "+updater("longVar").getAndIncrement(data));
    }


    class DataDemo {

        // 公共且可见的publicVar
        public volatile int publicVar = 0;
        // 保护级别的protectedVar
        protected volatile int protectedVar = 4;
        // 私有变量
        private volatile int privateVar = 5;

        // final 不可变量
        public final int finalVar = 11;

        public volatile Integer integerVar = 19;
        public volatile Long longVar = 18L;

    }

}

通过上述代码我们可以总结出CAS字段必须符合以下要求:

  • 变量必须使用volatile保证可见性
  • 必须是当前对象可以访问到的类型才可进行操作‘
  • 只能是实例变量而不是类变量,即不可以有static修饰符
  • 包装类也不行

七、CAS的ABA问题

CAS更新前会检查值有没有变化,如果没有变化则认为没人修改过,在进行更新操作。这种情况下,若我们A值修改为B,B再还原为A。这种修改再还原的操作,CAS是无法感知是否变化的,这就是所谓的ABA问题。

1.AtomicStampedReference源码详解

源码如下所示,可以看到AtomicStampedReference解决ABA问题的方式是基于当前修改操作的时间戳和元引用值是否一致,若一直则进行数据更新

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;
    ....
    
    /**
      * expectedReference :更新之前的原始引用值
      * newReference : 新值
      * expectedStamp : 预期时间戳
      * newStamp : 更新后的时间戳
      */
    public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
        // 获取当前的(元素值,版本号)对
        Pair<V> current = pair;
        return
            // 引用没变
            expectedReference == current.reference &&
            // 版本号没变
            expectedStamp == current.stamp &&
           //可以看到这个括号里面用了一个短路运算如果当前版本与新值一样就说更新过,就不往下走CAS代码了
            ((newReference == current.reference &&
           
            newStamp == current.stamp) ||
            // 构造新的Pair对象并CAS更新
            casPair(current, Pair.of(newReference, newStamp)));
    }

    private boolean casPair(Pair<V> cmp, Pair<V> val) {
        // 调用Unsafe的compareAndSwapObject()方法CAS更新pair的引用为新引用
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }

2.AtomicStampedReference解决ABA问题示例

代码示例,我们下面就用other代码模拟干扰现场,如果other现场先进行CAS更新再还原操作,那么main线程的版本号就会过时,CAS就会操作失败

/**
 * ABA问题代码示例
 */
public class AtomicStampedReferenceTest {
    private static AtomicStampedReference<Integer> atomicStampedRef =
            new AtomicStampedReference<>(1, 0);

    public static void main(String[] args) {
        Thread main = new Thread(() -> {
            System.out.println("操作线程" + Thread.currentThread() + ",初始值 a = " + atomicStampedRef.getReference());
            int stamp = atomicStampedRef.getStamp(); //获取当前标识别
            try {
                Thread.sleep(1000); //等待1秒 ,以便让干扰线程执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean isCASSuccess = atomicStampedRef.compareAndSet(1, 2, stamp, stamp + 1);  //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
            System.out.println("操作线程" + Thread.currentThread() + ",CAS操作结果: " + isCASSuccess);
        }, "主操作线程");

        Thread other = new Thread(() -> {
            Thread.yield(); // 确保thread-main 优先执行
            atomicStampedRef.compareAndSet(1, 2, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
            System.out.println("操作线程" + Thread.currentThread() + ",【increment】 ,值 = " + atomicStampedRef.getReference());
            atomicStampedRef.compareAndSet(2, 1, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
            System.out.println("操作线程" + Thread.currentThread() + ",【decrement】 ,值 = " + atomicStampedRef.getReference());
        }, "干扰线程");

        main.start();
        other.start();
    }

}

3.AtomicMarkableReference解决对象ABA问题

AtomicMarkableReference,它不是维护一个版本号,而是维护一个boolean类型的标记,标记对象是否有修改,从而解决ABA问题。

public boolean weakCompareAndSet(V       expectedReference,
                                     V       newReference,
                                     boolean expectedMark,
                                     boolean newMark) {
        return compareAndSet(expectedReference, newReference,
                             expectedMark, newMark);
    }

八、常见面试题

1.CAS为什么比synchronized快(重点)

CAS工作原理是基于乐观锁且操作是原子性的,与synchronized的悲观锁(底层需要调用操作系统的mutex锁)相比,效率也会相对高一些。

2.CAS是不是操作系统执行的?(重点)

不是,CAS是主要是通过处理器的指令来保证原子性的,在上面的讲解中我们都知道CAS操作底层都是调用Unsafe的native修饰的方法,以AtomicInteger为例对应的底层的实现是Unsafe的compareAndSwapInt:

 public final native boolean compareAndSwapInt(Object o, long offset,
                                                  int expected,
                                                  int x);

对应的我们给出这段代码的c语言实现,即位于:https://github.com/openjdk/jdk/blob/jdk8-b01/hotspot/src/share/vm/prims/unsafe.cpp的unsafe.cpp:

可以看到出去前两个形参后续的参数与compareAndSwapInt列表一一对应,这段代码执行CAS操作时,本质上就是调用cmpxchg指令(Compare and Exchange),cmpxchg指令会判断当前服务器是否是多核,如果是则讲LOCK前缀保证cmpxchg操作的原子性,反之就不加Lock前缀直接执行比对后修改变量值这种乐观锁操作。

对应源码如下,它首先获取字段的偏移地址,然后传入预期值e与原值比较,如果一致,则将新结果x写入原子操作变量内存中:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  //获取字段偏移量地址
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  //比较如果期望值e和当前字段存储的值一样,则讲值更新为x
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

3.CAS存在那些问题?

但即便如此CAS仍然存在两个问题:

(1) 可能存在长时间CAS:如下代码所示,这就是AtomicInteger底层的UNSAFE类如何进行CAS的具体代码   ,可以看出这个CAS操作需要拿到volatile变量后在进行循环CAS才有可能成功这就很可能存在自旋循环,从而给CPU带来很大的执行开销。

public final int getAndAddInt(Object paramObject, long paramLong, int paramInt)
  {
    int i;
    do
     //获取最新结果
      i = getIntVolatile(paramObject, paramLong);
      //通过cas自旋操作完成自增
    while (!compareAndSwapInt(paramObject, paramLong, i, i + paramInt));
    return i;
  }

(2) CAS只能对一个变量进行原子操作:为了解决这个问题,JDK 1.5之后通过AtomicReference使得变量可以封装成一个对象进行操作

ABA问题:总所周知CAS就是比对当前值与旧值是否相等,在进行修改操作,假设我现在有一个变量值为A,我改为B,再还原为A,这样操作变量值是没变的?那么CAS也会成功不就不合理吗?这就好比一个银行储户想查询概念转账记录,如果转账一次记为1,如果按照ABA问题的逻辑,那么这个银行账户转账记录次数有可能会缺少。为了解决这个问题JDK 1.5提供了AtomicStampedReference,通过比对版本号在进行CAS操作,那么上述操作就会变为1A->2B->3A,由于版本追加,那么我们就能捕捉到当前变量的变化了。

4.AtomicInteger自增到10000后如何归零

AtomicInteger atomicInteger=new AtomicInteger(10000);
atomicInteger.compareAndSet(10000, 0);

5.CAS 平时怎么用的,会有什么问题,为什么快,如果我用 for 循环代替 CAS 执行效率是一样的吗?(重点)

问题1: 一些需要并发计数并实时监控的场景可以用到。

 问题2: CAS存在问题:CAS是基于乐观锁机制,所以数据同步失败就会原地自旋,在高并发场景下开销很大,所以线程数很大的情况下不建议使用原子类。

 问题3:用 for 循环代替 CAS 存在问题: 如果并发量大的话,自旋的线程多了就会导致性能瓶颈。

 for 循环代替 CAS执行效率是否一样:大概率是CAS快,原因如下:

  • CAS是native方法更接近底层
  • for循环为了保证线程安全可能会用到sync锁或者Lock无论那种都需要上锁和释放的逻辑,相比CAS乐观锁来说开销很大。
责任编辑:赵宁宁 来源: 写代码的SharkChili
相关推荐

2022-12-06 08:42:28

2023-12-14 07:36:16

Java并发原子类

2024-08-09 08:41:14

2023-12-01 08:54:50

Java原子类型

2020-12-11 11:11:44

原子类JavaCAS

2023-01-05 12:30:32

Redis

2021-03-11 00:05:55

Java高并发编程

2021-03-04 07:24:24

JavaSemaphore高并发

2021-03-18 00:14:29

JavaCyclicBarri高并发

2009-03-09 21:25:11

Linuxnagios开源

2021-02-05 11:35:03

原子类数值变量

2024-04-22 00:00:00

CASCPU硬件

2022-12-06 17:28:36

Java优化CAS操作

2017-09-19 14:53:37

Java并发编程并发代码设计

2021-04-26 17:23:21

JavaCAS原理

2019-08-29 09:30:20

Java泛型构造器

2011-03-30 10:07:02

Zabbix安装

2010-09-25 13:07:50

DHCP协议结构

2010-07-28 22:20:10

RIP路由配置

2010-04-20 11:51:31

负载均衡
点赞
收藏

51CTO技术栈公众号