一、前言
我们的《小白系列》到目前为止,讲了以下主题内容:
- 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 开头的类中,对外提供了一个方法
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 源码:
找到了其对应的三个 JNI 方法的定义。在来看看具体的实现:
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变量中。