Java CAS原子操作详解

作者:飞奔的小付 时间:2023-05-06 15:37:15 

一.什么是CAS

CAS(Compare And Swap,比较并交换),通常指的是这样一种原子操作:针对一个变量,首先比较它的内存值与某个期望值是否相同,如果相同,就给它赋一个新值。

  • CAS是一个不可分割的原子操作,并且其原子性是直接在硬件层面得到保障的。

  • CAS是乐观锁(对比数据库的悲观、乐观锁)的一种实现方式,Java原子类中的递增操 作就通过CAS自旋实现的。

  • CAS是一种无锁算法,在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。

二.流程

Java CAS原子操作详解

三.应用

在 Java 中,CAS 操作是由 Unsafe 类提供支持的,该类定义了三种针对不同类型变量的 CAS 操作

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
   public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
   public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

它们都是 native 方法,由 Java 虚拟机提供具体实现,这意味着不同的 Java 虚拟机对它们的实现可能会略有不同。

以compareAndSwapInt为例,该方法提供四个参数,分别是:对象实例、内存偏移量、字段期望值、字段新值。该方法会针对指定对象实例中的相应偏移量的字段执行 CAS 操作。

public class CASTest {
   public static void main(String[] args) {
       Entity entity = new Entity();
       Unsafe unsafe = UnsafeFactory.getUnsafe();
       long offset = UnsafeFactory.getFieldOffset(unsafe, Entity.class, "x");
       //12
       System.out.println(offset);
       boolean successful;
       // 4个参数分别是:对象实例、字段的内存偏移量、字段期望值、字段更新值
       //x是不是等于0,如果等于0就把它修改为3
       successful = unsafe.compareAndSwapInt(entity, offset, 0, 3);
       System.out.println(successful + "-----" + entity.x);
       successful = unsafe.compareAndSwapInt(entity, offset, 3, 5);
       System.out.println(successful + "-----" + entity.x);
       successful = unsafe.compareAndSwapInt(entity, offset, 3, 8);
       System.out.println(successful + "-----" + entity.x);
   }
}
class Entity{
   int x;
}
public class UnsafeFactory {
   /**
    * 获取 Unsafe 对象
    * @return
    */
   public static Unsafe getUnsafe() {
       try {
           Field field = Unsafe.class.getDeclaredField("theUnsafe");
           field.setAccessible(true);
           return (Unsafe) field.get(null);
       } catch (Exception e) {
           e.printStackTrace();
       }
       return null;
   }
   /**
    * 获取字段的内存偏移量
    * @param unsafe
    * @param clazz
    * @param fieldName
    * @return
    */
   public static long getFieldOffset(Unsafe unsafe, Class clazz, String fieldName) {
       try {
           return unsafe.objectFieldOffset(clazz.getDeclaredField(fieldName));
       } catch (NoSuchFieldException e) {
           throw new Error(e);
       }
   }
}

将x做了三次修改,执行结果是:

12

true-----3

true-----5

false-----5

四.源码解析

Hotspot 虚拟机对compareAndSwapInt 方法的实现如下:

#unsafe.cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jo bject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
// 根据偏移量,计算value的地址
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
// Atomic::cmpxchg(x, addr, e) cas逻辑 x:要交换的值 e:要比较的值
//cas成功,返回期望值e,等于e,此方法返回true
//cas失败,返回内存中的value值,不等于e,此方法返回false
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

核心逻辑在Atomic::cmpxchg方法中,这个根据不同操作系统和不同CPU会有不同的 实现。这里我们以linux_64x的为例,查看Atomic::cmpxchg的实现

#atomic_linux_x86.inline.hpp
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint com pare_value) {
//判断当前执行环境是否为多处理器环境
int mp = os::is_MP();
//LOCK_IF_MP(%4)在多处理器环境下,为cmpxchgl指令添加lock前缀,以达到内存屏障的效果
 //cmpxchgl 指令是包含在 x86 架构及 IA‐64 架构中的一个原子条件指令,
 //它会首先比较 dest 指针指向的内存值是否和 compare_value 的值相等,
 //如果相等,则双向交换 dest 与 exchange_value,否则就单方面地将dest指向的内存值交给exchange_value。
 //这条指令完成了整个CAS操作,因此它也被称为CAS指令。
 __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
 : "=a" (exchange_value)
 : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
 : "cc", "memory");
 return exchange_value;
 }

需要注意的是cmpxchg有个隐含操作数eax,其实际过程是先比较eax的值(也就是 compare_value)和dest地址所存的值是否相等, 输出是"=a" (exchange_value),表示把eax中存的值写入exchange_value变量中。

Atomic::cmpxchg这个函数最终返回值是exchange_value,也就是说,如果cmpxchgl执行时compare_value和dest指针指向内存值相等则会使得dest指针指向内存值变成 exchange_value,最终eax存的compare_value赋值给了exchange_value变量,即函数最终返回的值是原先的compare_value。此时Unsafe_CompareAndSwapInt的返回值(jint) (Atomic::cmpxchg(x, addr, e)) == e就是true,表明CAS成功。如果cmpxchgl执行时 compare_value和(dest)不等则会把当前dest指针指向内存的值写入eax,最终输出时赋值给exchange_value变量作为返回值,导致(jint)(Atomic::cmpxchg(x, addr, e)) == e得到 false,表明CAS失败。

不管是 Hotspot 中的 Atomic::cmpxchg 方法,还是 Java 中的 compareAndSwapInt 方法,它 们本质上都是对相应平台的 CAS 指令的一层简单封装。CAS 指令作为一种硬件原语,有着天然 的原子性,这也正是 CAS 的价值所在。

五.缺点

CAS 虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方面:

  • 自旋 CAS 长时间地不成功,则会给 CPU 带来非常大的开销

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

  • ABA 问题

六.ABA 问题及解决方案

CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。

当有多个线程对一个原子类进行操作的时候,某个线程在短时间内将原子类的值A修改为B,又马上将其修改为A,此时其他线程不感知,还是会修改成功。

Java CAS原子操作详解

代码演示

public class ABATest {
   public static void main(String[] args) {
       AtomicInteger atomicInteger = new AtomicInteger(1);
       new Thread(()->{
           int value = atomicInteger.get();
           log.debug("Thread1 read value: " + value);
           // 阻塞1s
           LockSupport.parkNanos(1000000000L);
           // Thread1通过CAS修改value值为3
           if (atomicInteger.compareAndSet(value, 3)) {
               log.debug("Thread1 update from " + value + " to 3");
           } else {
               log.debug("Thread1 update fail!");
           }
       },"Thread1").start();
       new Thread(()->{
           int value = atomicInteger.get();
           log.debug("Thread2 read value: " + value);
           // Thread2通过CAS修改value值为2
           if (atomicInteger.compareAndSet(value, 2)) {
               log.debug("Thread2 update from " + value + " to 2");
               // do something
               value = atomicInteger.get();
               log.debug("Thread2 read value: " + value);
               // Thread2通过CAS修改value值为1
               if (atomicInteger.compareAndSet(value, 1)) {
                   log.debug("Thread2 update from " + value + " to 1");
               }
           }
       },"Thread2").start();
   }
}

Thread1 read value: 1
Thread2 read value: 1
Thread2 update from 1 to 2
Thread2 read value: 2
Thread2 update from 2 to 1
Thread1 update from 1 to 3

Thread1以为值没有更新过,还是将1更新为了3

解决:

数据库有个锁称为乐观锁,是一种基于数据版本实现数据同步的机制,每次修改一次数据,版本就会进行累加。 同样,Java也提供了相应的原子引用类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);
       }
   }
   ...

stamp是版本,每次修改可以通过+1保证版本唯一性。这样 就可以保证每次修改后的版本也会往上递增。

public class AtomicStampedReferenceTest {
   public static void main(String[] args) {
       // 定义AtomicStampedReference    Pair.reference值为1, Pair.stamp为1
       AtomicStampedReference atomicStampedReference = new AtomicStampedReference(1,1);
       new Thread(()->{
           int[] stampHolder = new int[1];
           int value = (int) atomicStampedReference.get(stampHolder);
           int stamp = stampHolder[0];
           log.debug("Thread1 read value: " + value + ", stamp: " + stamp);
           // 阻塞1s
           LockSupport.parkNanos(1000000000L);
           // Thread1通过CAS修改value值为3   stamp是版本,每次修改可以通过+1保证版本唯一性
           if (atomicStampedReference.compareAndSet(value, 3,stamp,stamp+1)) {
               log.debug("Thread1 update from " + value + " to 3");
           } else {
               log.debug("Thread1 update fail!");
           }
       },"Thread1").start();
       new Thread(()->{
           int[] stampHolder = new int[1];
           int value = (int)atomicStampedReference.get(stampHolder);
           int stamp = stampHolder[0];
           log.debug("Thread2 read value: " + value+ ", stamp: " + stamp);
           // Thread2通过CAS修改value值为2
           if (atomicStampedReference.compareAndSet(value, 2,stamp,stamp+1)) {
               log.debug("Thread2 update from " + value + " to 2");
               // do something
               value = (int) atomicStampedReference.get(stampHolder);
               stamp = stampHolder[0];
               log.debug("Thread2 read value: " + value+ ", stamp: " + stamp);
               // Thread2通过CAS修改value值为1
               if (atomicStampedReference.compareAndSet(value, 1,stamp,stamp+1)) {
                   log.debug("Thread2 update from " + value + " to 1");
               }
           }
       },"Thread2").start();
   }
}

Thread1 read value: 1, stamp: 1
Thread2 read value: 1, stamp: 1
Thread2 update from 1 to 2
Thread2 read value: 2, stamp: 2
Thread2 update from 2 to 1
Thread1 update fail!

因为版本不一样,Thread1没有将值修改成功,这就解决了ABA问题。

来源:https://blog.csdn.net/feibendexiaoma/article/details/127352910

标签:Java,CAS,原子操作
0
投稿

猜你喜欢

  • C++ 先对数组排序,在进行折半查找

    2021-07-07 09:03:18
  • Java关于MyBatis缓存详解

    2021-11-01 00:40:20
  • java虚拟机钩子关闭函数addShutdownHook的操作

    2021-10-18 00:58:25
  • springboot启动扫描不到dao层接口的解决方案

    2021-06-29 19:56:06
  • Java8 HashMap键与Comparable接口小结

    2023-11-29 10:10:31
  • Java集合定义与用法实例总结【Set、List与Map】

    2023-11-21 08:26:40
  • Android开发中Bitmap高效加载使用详解

    2021-06-05 01:32:15
  • Java封装、继承、多态三大特征的理解

    2023-07-16 14:26:46
  • Java Socket实现多人聊天系统

    2023-08-08 04:44:35
  • Android中Blade的使用方法

    2023-04-25 11:29:54
  • Maven将代码及依赖打成一个Jar包的方式详解(最新推荐)

    2022-03-31 06:52:47
  • C#利用System.Uri转URL为绝对地址的方法

    2021-06-04 13:41:40
  • 详解Java对象结构与对象锁的升级

    2021-12-05 16:18:38
  • ImageSwitcher图像切换器的使用实例

    2022-10-29 13:16:02
  • Socket通信原理和实践

    2022-07-05 02:42:31
  • 浅谈java 执行jar包中的main方法

    2022-06-28 07:58:27
  • 详解Android Activity之间切换传递数据的方法

    2021-10-28 04:55:53
  • 详解C++ bitset用法

    2022-10-30 08:57:16
  • Spring Security认证提供程序示例详解

    2022-11-07 18:06:40
  • spring整合JMS实现同步收发消息(基于ActiveMQ的实现)

    2022-06-09 06:00:36
  • asp之家 软件编程 m.aspxhome.com