看图学源码之 CAS源码解析

案例引入

不使用任何锁机制

public class MyLock {

    private static Unsafe unsafe;

    public Unsafe getUnsafe() {
        return unsafe;
    }
    public int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    public void inc(){
        value++;
    }
  
    public static void main(String[] args) {
        MyLock myLock = new MyLock();
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                for (int j = 0; j < 100; j++) {
                    myLock.inc();
                }
            }).start();
        }
        System.out.println(myLock.getValue());
    }

}
9882

使用CAS + 自旋的方式的方式执行

public class MyLock {
    private static long stateOffset;
   private static Unsafe unsafe;

    public Unsafe getUnsafe() {
        return unsafe;
    }
    public int value;

    public int getValue() {
        return value;
    }


    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
            stateOffset = unsafe.objectFieldOffset(MyLock.class.getDeclaredField("value"));
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

  
    public void casInc(){
        int intVolatile = 0;
        do{
            intVolatile = unsafe.getIntVolatile(this, stateOffset);
        }while(!unsafe.compareAndSwapInt(this,stateOffset,intVolatile,intVolatile+1));
    }
    public static void main(String[] args) {
        MyLock myLock = new MyLock();
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                for (int j = 0; j < 100; j++) {
                    myLock.casInc();
                }
            }).start();
        }
        LockSupport.parkNanos(1*1000*1000);
        System.out.println(myLock.getValue());
    }
}
10000

没加屏障,程序不会停止

class TestFence_{

    private static int a = 0;
    public static void main(String[] args) {
        new Thread(()->{
            while(a == 0){
            
            }
        }).start();
       LockSupport.parkNanos(1*1000*1000);
       TestFence_.a = 1;
    }
}


// 不会停止

加了屏障,程序会停止

class TestFence_{

    private static int a = 0;
    public static void main(String[] args) {
        new Thread(()->{
            while(a == 0){
              //加屏障: 禁止使用编译器进行优化
               MyLock.getUnsafe().loadFence();
            }
        }).start();
       LockSupport.parkNanos(1*1000*1000);
       TestFence_.a = 1;
    }

}

CAS 基本介绍

Java 中的 CAS 操作本质上还是通过 sun.misc.Unsafe 类来实现,因为 CAS 操作需要直接操作内存,而 Unsafe 类恰好提供了直接操作内存的方法。

主要提供了三个参数 : 内存地址V ; 旧值 A,新值B;如果当前内存地址的值等于 原始值 A,则将内存地址的值修改为 新值 B,否则不进行任何操作。

cas 的操作是原子的,就是在同一个时刻只有一个线程能够成功执行这个操作。

CAS 源码解析

在这里插入图片描述

java

Unsafe.java 所在目录 :openjdk-jdk8u-master/jdk/src/share/classes/sun/misc/Unsafe.java


public final class Unsafe {
...
 public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
...
}

cpp

Unsafe.cpp 所在目录 :openjdk-jdk8u-master/hotspot/src/share/vm/prims/unsafe.cpp

//作用是比较和交换整型值,用于实现线程安全的原子操作。

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
//代码首先使用JNIHandles::resolve函数将传入的obj对象转换为一个oop对象
  oop p = JNIHandles::resolve(obj);
//然后使用index_oop_from_field_offset_long函数根据偏移量offset来获取  整型地址addr
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
//使用Atomic::cmpxchg函数来比较地址addr处的值和e的值,如果相等,则将x的值赋给addr。
														//新值,地址值,旧值   
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e; //代码将比较结果与旧的值e进行比较,并返回比较结果的布尔值。
UNSAFE_END

linux -内联汇编

  • exchange_value——新值
  • dest——目标地址
  • compare_value ——旧值
// 使用cmpxchgl指令比较并交换两个整数值。
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {

  int mp = os::is_MP();
    //将exchange_value与dest指向的整数值进行比较,如果相等,则将exchange_value存储到dest指向的内存位置,并返回原本在dest中的值
  
  //通过__asm__ volatile语句 表示 ASM汇编,禁止编译器优化,将汇编代码嵌入到C++代码中,实现了比较并交换的原子操作
  /*
  ASM的内敛汇编格式:
  asm( assembler template:就是cmpxchgl%1,(%3)表示汇编模板
  		:output operands:表示输出操作数,=a对应eax寄存器
  		:input operands:表示输入参数,
  			%1就是exchange_value(新值),
  			%3是dest(变量地址),
  			%4就是mp(多处理器标志),
  			
  			r表示任意寄存器,
  			a是eax寄存器(存的是compare_value的值,原值)
  		:list of clobbered registers:就是些额外参数,cc表示编译器cmpxchgl的执行将影响到标志寄存器,memory告诉
编译器要重新从内存中读取变量的最新值,这点实现了volatile
  )
  */
  //LOCK_IF_MP宏 根据系统是否为多处理器(MP)来决定是否在指令前加上lock前缀,以确保操作的原子性。
  
  //#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "    多核才会加 lock  锁总线
  __asm__ volatile (LOCK_IF_MP(%4) 
   //cmpxchgl指令就是比较eax寄存器的值(compare_value预期值)和目的地址((dest))的值是否相同
   //   %1 和 %3 比较替换,实际上 %1 是会用到 %2进行比较的,%1主要是用与交换的,或者说是用于在可以交换的时候存储在目的地址的值
                    "cmpxchgl %1,(%3)" 
  //如果相同,就把源操作数(exchange_value)的值赋值给目的地址的值((dest)),并且设置标志位ZF=1(影响的cc);
  //如果不同,就把源操作数(exchange_value)的值赋值给eax寄存器,并且设置ZF=0

/*
:"=a" (exchange_value):这是输出操作数列表,表示将%eax寄存器中的值(即exchange_value)作为输出。

: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp):这是输入操作数列表,
表示将exchange_value、compare_value、dest和mp分别赋值给  %ecx、  %eax、  %edx和  %ebx寄存器。
*/
                    : "=a" (exchange_value)   // 0%
                    : "r" (exchange_value), // 1%  ecx寄存器  
                    "a" (compare_value), // 2%     eax寄存器
                    "r" (dest),// 3%               edx寄存器
                    "r" (mp)// 4%                  ebx寄存器
                    
/*  "cc", "memory":这是指令的副作用列表,表示该指令可能会修改条件码寄存器(cc)和内存。*/
                    : "cc", "memory");
  return exchange_value;
}

windows-内联汇编

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  // alternative for InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest            获取内存地址 dest 数据放至 edx 寄存器中
    mov ecx, exchange_value  将 新值exchange_value  放入到 ecx 寄存器中
    mov eax, compare_value   将 原值compare_value   放入到 eax 寄存器中
    LOCK_IF_MP(mp)           根据当前cpu是否为多核 决定 是不是要 加锁
    cmpxchg dword ptr [edx], ecx
      // dword   doubleword     双字
      // ptr     pointer,与前面的 dword 连起来使用,表明访问的内存单元是一个双字单元
      // [edx]   表示一个内存单元,edx 是寄存器,dest 指针值存放在 edx 中
      // [edx]   表示内存地址为 dest 的内存单元
// 访问内存地址为 dest 的双字内存单元,
// 就是说:将 ecx 寄存器中的值与 内存地址为 dest 的双字内存单元 进行比较。如果两个值相等,则将ecx寄存器中的值存储到目标操作数中。
//实际上  进行比较的值 还是 eax 中的compare_value,要是相等的话,才会使用 exchange_value 在可以交换的时候存储到目的地址的值
  }
}

CAS的问题

1、ABA 问题

基本介绍:

在执行CAS操作时,如果内存中的值从A变为B,然后再变回A,那么CAS操作会错误地认为内存中的值没有发生变化,导致可能出现意外的结果。

后果:

可能会导致一些潜在的问题,例如在无锁数据结构中,一个线程可能在执行CAS操作时,另一个线程修改了内存中的值,然后又恢复为原始值,这样第一个线程就无法察觉到这个变化。

解决:

1、使用带有版本号的CAS操作。每当对内存进行修改时,都将版本号递增,这样即使值发生了变化,但版本号也会发生变化,从而避免了ABA问题的发生。

2、JDK的解决:AtomicStampedReference

//如果当前引用 == 预期引用 并且 当前标记等于预期标记,则以原子方式将引用和标记的值设置为给定更新值。
public boolean weakCompareAndSet(V   expectedReference,
                                     V   newReference,
                                     int expectedStamp,
                                     int newStamp) {
        return compareAndSet(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 &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }
2、循环开销

在底层汇编或者cpp代码的层面并没有循环开销的问题;

但是在java 应用层面,有时候写的代码就会产生这样的循环开销的问题,就是说CAS操作通常是通过一个循环来实现的,因为它需要不断地重试,直到成功为止。
也就是在高并发情况下,多个线程同时对同一个变量进行CAS操作时,可能会发生竞争,导致大量的重试。这种情况下,CAS操作的循环会不断地重试,消耗了大量的CPU资源和时间

解决

使用指数退避策略,即在每次重试时增加等待时间,以减少对共享资源的竞争。另外,可以考虑使用其他并发控制手段,如锁机制,来避免过多的CAS操作

内存屏障源码解析

Java

public native void loadFence();

cpp

// 总的来说,这段代码的作用是执行一个加载屏障操作,以确保后续的读取操作能够看到之前的最新值。


在这段代码中,Unsafe_LoadFence函数被标记为UNSAFE_ENTRY,这表示它是一个不安全的入口点,可能会涉及到一些底层的操作或者绕过一些安全检查。
UNSAFE_ENTRY(void, Unsafe_LoadFence(JNIEnv *env, jobject unsafe))
  
  //UnsafeWrapper("Unsafe_LoadFence")是一个函数调用,它可能是用来处理一些与安全性相关的操作或者包装底层的不安全代码。
  UnsafeWrapper("Unsafe_LoadFence");
  //通过调用OrderAccess::acquire()函数来执行一个加载屏障(load fence)操作。
  //加载屏障是一种同步原语,用于确保在  加载操作之后  的所有  读取操作都能  看到加载操作之前  的最新值。

	OrderAccess::acquire();
UNSAFE_END

内联汇编

// 编译器屏障
//目的是通过内联汇编语句来实现内存顺序的同步,以确保在多线程环境下的正确性和一致性。
  //acquire函数通过使用内联汇编语句来实现内存顺序的同步。
inline void OrderAccess::acquire() {
  //声明了一个volatile intptr_t类型的本地变量local_dummy,用于存储从内存中读取的值。
  volatile intptr_t local_dummy;
  //根据编译器宏定义的架构类型,使用不同的汇编指令来将栈顶或栈指针指向的内存数据加载到local_dummy变量中。
#ifdef AMD64
  //在x86架构中(非AMD64),使用movl指令将栈顶(ESP)指向的内存数据加载到local_dummy中。
  __asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) : : "memory");
#else
  //在AMD64架构中,使用movq指令将栈指针(RSP)指向的内存数据加载到local_dummy中。
  __asm__ volatile ("movl 0(%%esp),%0" : "=r" (local_dummy) : : "memory");
  
//通过使用volatile关键字修饰local_dummy变量,可以确保编译器不会对该变量的读取进行优化,从而保证了内存顺序的同步操作。
#endif // AMD64
}


  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值