Java小白系列(八):乐观锁CAS

一、前言

我们的《小白系列》到目前为止,讲了以下主题内容:

  • synchronized 同步代码块;
  • volatile 变量可见;
  • JOL(Java Object Layout)对象布局;
  • JMM(Java Memory Model)内存模型;
  • Happens-Before规则;

之所以花了大量的时间精力来普及以上内容,一方面是为了让大家更加全面细致的掌握这些基础知识,另一方面,也是为我们接下来的内容做好准备,打好地基。

从本篇开始,虽然依然是普及小白知道,但是,我们的内容将涉及到『锁』,JDK1.5开始,加入了 concurrent 包,包中也提供了几种锁,通过我的讲解,希望大家能更快的掌握它们,能根据不同的情况来选择最合理的『锁』来实现自己的需求,达到最佳的性能。

那么,我们就正式进入『锁』系列的第一篇内容:乐观锁 CAS( CompareAndSwap )。

二、CAS

从字面意思上理解:比较并交换!这是什么意思呢?
当我们想修改一个对象的值的时候,我想先比较一下该对象是否是我们期望的值,如果是的,那么就用我们新的值与期望(现有)的值进行交换,即修改这个对象的值。

为什么需要先比较一下呢?我们看个例子:

public class Demo {
    private int x = 10;

    public void increase() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        x ++;
        System.out.println(Thread.currentThread() + ", x = " + x);
    }

    public static void main(String[] args) {
        Demo demo = new Demo();
        new Thread(demo::increase).start();
        new Thread(demo::increase).start();
    }
}

我们期望两个线程去执行『increase』方法,对 x 的值 +1,期望两次操作手,x 的值为 12,我们看下可能的结果:

// 结果1
Thread[Thread-0,5,main], x = 11
Thread[Thread-1,5,main], x = 11

// 结果2
Thread[Thread-0,5,main], x = 11
Thread[Thread-1,5,main], x = 12

很显然,结果1不是我们想要的,结果2才是我们想要的。相信大家知道原因了,因为两个线程并发执行,x 的副本在各自的工作线程中修改,所以使得 x 的值不可见,最简单最轻量级的办法就是加上 volatile 修饰符,其次就是用 synchronized 来同步。

那么除此,是否还有其它方式呢?

当然有,CAS 比较再交换的方式,就是我们想要的一种轻量锁方式,CAS 需要两个输入:E 和 N,即『expect value』和『update value』。我们在更新对象之前,期望当前对象的值是我们期望的值(expect value),只有是期望的值,才表明没有其它线程修改过,那么我们可以去修改它;如果当前对象的值与我们期望的不符,那我们就不能去更新它,否则就会使得该对象的数据变成脏数据。

JDK1.5 为我们提供了一组原子操作的类,在 concurrent / atomic 目录下,以 Atomic 开头的类:

atomic.png

Atomic 开头的类中,对外提供了一个方法

public class AtomicXXXX {
    public final boolean compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
    }
}

这个就是CAS,它由 Unsafe 类提供。

我们可以看一下 AtomicInteger 源码:

public class AtomicInteger extends Number implements java.io.Serializable {
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    } 
    
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }
}
public final class Unsafe {
    public final int getAndSetInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var4));
    
        return var5;
    }
}

无论是直接使用 AtomicInteger.compareAndSet 还是 AtomicInteger.getAndSet,最终都会调用 Unsafe 的 compareAndSwapInt 方法来完成我们的更新操作,只不过,前者直接调用是程序给定期望条件且满足才更新,而后者是强制更新。

那么,我们来修改一下我们的例子:

public class CASDemo {
    private AtomicInteger x = new AtomicInteger(10);

    public void increase() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        x.getAndIncrement();
        System.out.println(Thread.currentThread() + ", x = " + x);
    }

    public static void main(String[] args) {
        CASDemo demo = new CASDemo();

        new Thread(demo::increase).start();
        new Thread(demo::increase).start();
    }
}

再次打印输出结果:

Thread[Thread-0,5,main], x = 11
Thread[Thread-1,5,main], x = 12

达到我们期望的效果。

三、CAS 底层实现

3.1、Unsafe

我们在《小白四:JOL》一文中,有提到 Unsafe 类的『objectFieldOffset』就是获取该对象实例中某个字段所在内存的位置(相对于该对象内存地址的偏移量),该方法是 native 方法,由底层 C++ 实现。

除此,Unsafe 还提供了 CAS 操作,即『compareAndSwap』乐观锁方法,我们再看看一下 AtomicInteger中的CAS方法:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    private volatile int value;
    
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
}

从该方法中,我们看到了几点信息:

  • valueOffset 是 value 成员变量的内存地址;
  • expect 是我们期望 value 的值;
  • update 是我们想要更新(修改)的值;

而 unsafe.comparenAndSwapInt 是个 native 方法:

public final class Unsafe {
    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);
    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
}

3.2、Unsafe.compareAndXXX 的 native 实现

我们查看 JDK1.8 的 native 源码:

CAS_X.png

找到了其对应的三个 JNI 方法的定义。在来看看具体的实现:

unsafe.png

3.3、Unsafe_CompareAndSwapInt

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  
  // 获取对象,如 AtomicInteger
  oop p = JNIHandles::resolve(obj);
  
  // 获取指定的偏移地址
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  
  // 重点,Atomic::cmpxchg(x, addr, e)
  // 如果偏移地址所存的值是 e,则用 x 替换,并返回替换前偏移地址的值
  // 若返回的值 == e,则表明更新成功
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

Atomic::cmpxchg 方法与OS CPU相关,我们可以查看 hotspot/os_cpu下目录,我们以 linux_x86/vm/atomic_linux_x86.inline.hpp为例:

#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value) {
  int mp = os::is_MP(); // CPU个数 > 1时,mp = 1,否则 mp = 0
  __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;
}

这里是内嵌汇编指令(C与C++代码混合在一起时),我们看到,在多CPU下,cmpxchg也会加锁,即宏『LOCK_IF_MP』的展开后计算结果。

3.4、内嵌汇编了解

  • asm 表示后面的代码为内嵌汇编;
  • volatile 表示编译器不要优化代码,后面的指令保留原样;volatile是 volatile 的别名;
  • cmpxchg 是汇编指令,表示比较并交换操作数
  • cmpxchgl 加了 l 表示4字节;

内嵌汇编模板

 内嵌汇编模板
 asm ( assembler template
       : output operands               (optional)
       : input operands                (optional)
       : list of clobbered registers   (optional)   
);

输入,即 input 是:
"r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
表示 compare_value 存入 eax 寄存器;而 exchange_value,dest,mp 的值可以存入任意通用寄存器中;

嵌入式汇编规定把输出和输入寄存器按统一顺序编号:顺序是从输出寄存器序列从左到右从上到下以“%0”开始,分别记为 %0、%1 ··· %9;也就是说,输出的eax是%0,输入的exchange_value、compare_value、dest、mp分别是%1、%2、%3、%4。

因此,cmpxchgl %1,(%3)实际上表示cmpxchgl exchange_value,(dest),此处(dest)表示dest地址所存的值。

需要注意的是cmpxchgl有个隐含操作数eax,其实际过程是先比较eax的值(也就是compare_value)和dest地址所存的值是否相等,如果相等则把exchange_value的值写入dest指向的地址。如果不相等则把dest地址所存的值存入eax中。

输出是"=a" (exchange_value),表示把eax中存的值写入exchange_value变量中。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容