一、CAS

  1. CAS3个操作数,位置内存值N,旧的预期值A,要修改的更新值B,当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来

CAS - 图1

  1. private static final long valueOffset;
  2. static {
  3. try {
  4. valueOffset = unsafe.objectFieldOffset
  5. (AtomicInteger.class.getDeclaredField("value"));
  6. } catch (Exception ex) { throw new Error(ex); }
  7. }
  8. //this:当前对象 valueOffset:当前对象在内存中的偏移量 expect:期望值 update:更新后的值
  9. public final boolean compareAndSet(int expect, int update) {
  10. return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
  11. }
  1. public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

1.1Unsafe类

  1. CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为JavaCAS操作的执行依赖于Unsafe类的方法。
  2. 注意:Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底昃资源执行相应任务

1.2CAS如何保证原子操作的

  1. CAS的全称为Compare-And-Swap,它是一条CPU并发原语。
  2. 它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
  3. AtomicInteger类主要利用CAS(compare and swap) + volatile native方法来保证原子操作,从而避免 synchronized的高开销,执行效率大为提升。
  1. public final int getAndAddInt(Object o, long offset, int delta) {
  2. int v;
  3. do {
  4. v = getIntVolatile(o, offset);
  5. } while (!compareAndSwapInt(o, offset, v, v + delta));
  6. return v;
  7. }

volatile不能保证操作的原子性,这就会导致多个线程在修改同一个变量时,由于缓存一致性协议会导致修改失效

  1. public class VolatileNoAtomicDemo
  2. {
  3. public static void main(String[] args) throws InterruptedException
  4. {
  5. MyNumber myNumber = new MyNumber();
  6. for (int i = 1; i <=10; i++) {
  7. new Thread(() -> {
  8. for (int j = 1; j <= 1000; j++) {
  9. myNumber.addPlusPlus();
  10. }
  11. },String.valueOf(i)).start();
  12. }
  13. //暂停几秒钟线程
  14. try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
  15. System.out.println(Thread.currentThread().getName() + "\t" + myNumber.number);
  16. }
  17. }
  18. class MyNumber
  19. {
  20. volatile int number = 0;
  21. public void addPlusPlus()
  22. {
  23. number++;
  24. }
  25. }

使用AtomicInteger是如果当前修改失败会进行重试

  1. @Slf4j
  2. public class CASTest {
  3. public static void main(String[] args) throws InterruptedException {
  4. AtomicInteger nu = new AtomicInteger();
  5. for (int i = 0; i < 10; i++) {
  6. new Thread(() -> {
  7. for (int j = 0; j < 1000; j++) {
  8. nu.getAndIncrement();
  9. }
  10. }).start();
  11. }
  12. Thread.sleep(1000);
  13. log.info("num的值为{}",nu);
  14. }
  15. }

二.底层实现

Unsafe.java类

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

unsafe.cpp

  1. UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  2. UnsafeWrapper("Unsafe_CompareAndSwapInt");
  3. oop p = JNIHandles::resolve(obj);
  4. //拿到变量value在内存中的地址,根据偏移量valueOffsht,计算 value 的地址
  5. jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  6. //调用Atomic中的函数cmpxchg来进行比较交换,其中参数x是即将更新的值,参数e是原内存的值
  7. return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
  8. UNSAFE_END

atomic.cpp

  1. unsigned Atomic::cmpxchg(unsigned int exchange_value,
  2. volatile unsigned int* dest, unsigned int compare_value) {
  3. assert(sizeof(unsigned int) == sizeof(jint), "more work to do");
  4. // 根据操作系统类型调用不同平台下的重载函数,这个在预编译期间编译器会决定调用哪个平台下的重载函数
  5. return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest,
  6. (jint)compare_value);
  7. }

windows系统atomic_windows_x86.inline.hpp

  1. inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
  2. // alternative for InterlockedCompareExchange
  3. int mp = os::is_MP();
  4. __asm {
  5. //三个move指令表示的是将后面的值移动到前面的寄存器上
  6. mov edx, dest
  7. mov ecx, exchange_value
  8. mov eax, compare_value
  9. //CPU原语级别,CPU触发
  10. LOCK_IF_MP(mp)
  11. //比较并交换指令
  12. //cmpxchg:即“比较并交换"指令
  13. //dword:全称是double word表示两个字,一共四个字节
  14. //ptr:全称是pointer,与前面的dword连起来使用,表明访问的内存单元是一个双字单元
  15. //将eax寄存器中的值(compare_value)与[edx]双字内存单元中的值进行对比,
  16. //如果相同,则将ecx寄存器中的值( exchange_value)存入[edx]内存单元中
  17. cmpxchg dword ptr [edx], ecx
  18. }
  19. }

三、CAS的缺点

3.1 导致cpu空转

  1. 如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

3.2 ABA问题

  1. CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
  2. 比如说一个线程A从内存位置V中取出num=1,这时候另一个线程B也从内存中取出num=1,并且线程B进行了一些操作将值变成了2
  3. 然后线程B又将V位置的数据变成1,这时候线程A进行CAS操作发现内存中仍然是1,然后线程A操作成功。

解决:加版本号,不仅比较当前值还要比较当前版本(AtomicStampedReference)