汇编指令CMPXCHG

  • 在了解CAS之前我们先看看汇编指令CMPXCHG,CMPXCHG的完整名称是Compare and Exchange,CMPXCHG汇编指令的使用方法如下:
    cmpxchg dest,src
    将AL、AX、EAX或RAX寄存器中的值与第一个操作数dest(目标操作数)进行比较。
    如果两个值相等,则将第二个操作数src(源操作数)加载到目标操作数中。
    如果不相等,则目标操作数被加载到AL、AX、EAX或RAX寄存器中。 RAX寄存器仅在64位模式下可用。

  • 需要注意的是汇编指令cmpxchg并不是一个原子操作,想要保证原子性,通常需要加上Lock前缀。
    对多核和多处理器,LOCK前缀都会保证指令的原子性。
    对共享内存,通过LOCK#信号在指令操作期间,锁住系统总线,来保证原子性。
    对于已经被缓存的数据,通过Cache lock来保证原子性。Cache lock是通过缓存一致性协议,来保证修改的原子性,而不是通过总线锁,这样可以提高性能。

什么是CAS?

CAS全称Compare And Swap,比较与交换,是乐观锁的主要实现方式。CAS可以在不使用java同步锁的情况下实现多线程之间的变量同步。ReentrantLock内部的AQS和原子类内部都使用了CAS。CAS算法涉及到三个操作数:需要读写的内存值V。进行比较的值A。要写入的新值B。只有当V的值等于A时,才会使用原子方式用新值B来更新V的值。以AtomicInteger为例,AtomicInteger的getAndIncrement()方法底层就是CAS实现,关键代码是 compareAndSwapInt(obj, offset, expect, update),其含义就是,如果obj内的value和expect相等,就证明没有其他线程改变过这个变量,那么就更新它为update,如果不相等,那就会继续重试直到成功更新值。

以AtomicInteger的getAndIncrement()方法为例来这个方法是如何保证可见性的

实际效果查看使用AtomicInteger和不使用AtomicInteger在多线程环境下进行自加操作的结果。

static int org = 0;
static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
    // 本例中只是用为等待10个线程执行结束 main线程拿到最终结果。
    final CountDownLatch cdl = new CountDownLatch(10);
    // 初始化10个线程
    for (int i = 1; i <= 10; i++){
        new Thread(() -> {
            // 每个线程执行++100次
            for (int j = 1; j<=100; j++){
                atomicInteger.getAndIncrement();
                org++;
            }
            cdl.countDown();
        }).start();
    }
    cdl.await();
    System.out.println("atomicInteger 10个线程++100次的结果:"+atomicInteger.get());
    System.out.println("普通 10个线程++100次的结果:"+org);
}


可以发现在多线程下普通的使用i++不能保证原子性,会出现数据覆盖问题。

接下来看看getAndIncrement()方法是如何保证原子性的。

//AtomicInteger类
//变量value; 相当于i++中的i
private volatile int value;
//创建Unsafe类的实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
//变量value的偏移量, 具体赋值是在下面的静态代码块中中进行的
private static final long valueOffset;
//在静态代码块中获取变量value的偏移量
static {
    try {
    	//获取value变量的偏移量, 赋值给valueOffset
        valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

//执行value++操作
public final int getAndIncrement() {
		//this是当前对象, valueOffset是一个用于指定共享变量在对象内存布局中的偏移量的参数。它允许直接使用对象的内存表示,而不是通过对象的引用来进行操作。
        return unsafe.getAndAddInt(this, valueOffset, 1);
}

//Unsafe类
//获取内存地址为obj+offset的变量值, 并将该变量值加上delta
public final int getAndAddInt(Object obj, long offset, int delta) {
    int v;
    do {
    	//通过对象和偏移量获取变量的值
    	//由于volatile的修饰, 直接获取内存中的值,还有个getInt()方法,该方法不是直接从内存中读取,而是先去命中缓存。
        v= this.getIntVolatile(obj, offset);
    /*
	while中的compareAndSwapInt()方法尝试修改v的值,具体地, 该方法也会通过obj和offset获取变量的值
	如果这个值和v不一样, 说明其他线程修改了obj+offset地址处的值, 此时compareAndSwapInt()返回false, 继续循环
	如果这个值和v一样, 说明没有其他线程修改obj+offset地址处的值, 此时可以将obj+offset地址处的值改为v+delta, compareAndSwapInt()返回true, 退出循环
	Unsafe类中的compareAndSwapInt()方法是原子操作, 所以compareAndSwapInt()修改obj+offset地址处的值的时候不会被其他线程中断,
	这个原子操作是通过汇编指令CMPXCHG加上Lock前缀(单处理器,单核不需要Lock前缀)保证的,加上Lock前缀会去锁定缓存行或者总线,保证当前内存区域只允许一个线程访问。
	*/
    } while(!this.compareAndSwapInt(obj, offset, v, v + delta));

    return v;
}
posted on 2023-06-29 20:34  旅途的痕迹  阅读(556)  评论(0)    收藏  举报