LOCK前缀指令篇(二):JavaCAS原理及AtomicInteger的实现目录LOCK前缀指令篇(二):JavaCAS原理及AtomicInteger的实现一、Java的CAS
LOCK前缀指令篇(二):Java CAS原理及AtomicInteger的实现
目录
LOCK前缀指令篇(二):Java CAS原理及AtomicInteger的实现
一、Java的CAS实现原理
二、AtomicInteger的实现原理
一、Java的CAS实现原理
JAVA中的CAS操作都是通过sun包下Unsafe类实现,而Unsafe类中的方法都是native方法,由JVM本地实现,笔者为了弄清楚真正的实现原理,以AtomicInteger(需要CAS操作)为例,查看了openJDK7的源码:
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
// 根据偏移量,计算 value 的地址。这里的 offset 就是 AtomaicInteger 中的 valueOffset
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
// 调用 Atomic 中的函数 cmpxchg,该函数声明于 Atomic.hpp 中
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
很明显,Unsafe_CompareAndSwapInt函数实现的关键在于最后一句,使用了cmpxchg指令,进一步,我们来看看Windows平台下Atomic::cmpxchg 函数:
// atomic_windows_x86.inline.hpp
#define LOCK_IF_MP(mp) __asm cmp mp, 0 \
__asm je L0 \
__asm _emit 0xF0 \
__asm L0:
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
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}
上面的代码由 LOCK_IF_MP 预编译标识符和 cmpxchg 函数组成。为了看到更清楚一些,我们将 cmpxchg 函数中的 LOCK_IF_MP 替换为实际内容。如下:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
// 判断是否是多核 CPU
int mp = os::is_MP();
__asm {
// 将参数值放入寄存器中
mov edx, dest // 注意: dest 是指针类型,这里是把内存地址存入 edx 寄存器中
mov ecx, exchange_value
mov eax, compare_value
// LOCK_IF_MP
cmp mp, 0
/*
* 如果 mp = 0,表明是线程运行在单核 CPU 环境下。此时 je 会跳转到 L0 标记处,
* 也就是越过 _emit 0xF0 指令,直接执行 cmpxchg 指令。也就是不在下面的 cmpxchg 指令
* 前加 lock 前缀。
*/
je L0
/*
* 0xF0 是 lock 前缀的机器码,这里没有使用 lock,而是直接使用了机器码的形式。至于这样做的
* 原因可以参考知乎的一个回答:
* https://www.zhihu.com/question/50878124/answer/123099923
*/
_emit 0xF0
L0:
/*
* 比较并交换。简单解释一下下面这条指令,熟悉汇编的朋友可以略过下面的解释:
* cmpxchg: 即“比较并交换”指令
* dword: 全称是 double word,在 x86/x64 体系中,一个
* word = 2 byte,dword = 4 byte = 32 bit
* ptr: 全称是 pointer,与前面的 dword 连起来使用,表明访问的内存单元是一个双字单元
* [edx]: [...] 表示一个内存单元,edx 是寄存器,dest 指针值存放在 edx 中。
* 那么 [edx] 表示内存地址为 dest 的内存单元
*
* 这一条指令的意思就是,将 eax 寄存器中的值(compare_value)与 [edx] 双字内存单元中的值
* 进行对比,如果相同,则将 ecx 寄存器中的值(exchange_value)存入 [edx] 内存单元中。
*/
cmpxchg dword ptr [edx], ecx
}
}
综上,可以发现,CAS 的实现离不开处理器的支持。以上这么多代码,其实核心代码就是一条带lock 前缀的 cmpxchg 指令,即
lock cmpxchg dword ptr [edx], ecx
程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。
在所有的 X86 CPU 上都具有锁定一个特定内存地址的能力,当这个特定内存地址被锁定后,它就可以阻止其它的系统总线读取或修改这个内存地址。这种能力是通过 LOCK 指令前缀再加上具体操作(如上面的 cmpxchg)的汇编指令来实现的。当使用 LOCK 指令前缀时,它会使 CPU 宣告一个 LOCK# 信号,这样就能确保在多处理器系统或多线程竞争的环境下互斥地使用这个内存地址。当指令执行完毕,这个锁定动作也就会消失。
处理器使用嗅探技术保证它的内部缓存、系统内存和其它处理器的缓存的数据在总线上保持一致。例如CPU A嗅探到CPU B打算写内存地址,且这个地址处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充(即重新从主内存中加载共享变量)。
如上所述,LOCK前缀指令保障了cmpxchg(CAS)的原子性。
二、AtomicInteger的实现原理
查看AtomicInteger的源码,其中有如下内容:
private volatile int value;
public final boolean compareAndSet(int expect, int update)
{
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
结合上一篇文章《LOCK前缀指令篇(一):Java 之深入浅出解读 volatile》,从上面的代码中不难分析出AtomicInteger的实现原理:通过声明一个volatile (借助LOCK指令的 “内存锁定”,保证同一时刻只有一个线程可以修改内存值)修饰的变量value,再加上unsafe.compareAndSwapInt的方法,来保证实现线程同步的。
顾名思义,compareAndSwapInt的方法其实就是CAS操作,它是一个JNI(Java Native Interface)方法,其底层实现也用到了LOCK前缀指令,确保操作的原子性。
CAS指令在Intel CPU上称为CMPXCHG指令,它的作用是将指定内存地址的内容与所给的某个值相比,如果相等,则将其内容替换为指令中提供的新值,如果不相等,则更新失败。这一比较并交换的操作是原子的,不可以被中断。CAS包含了读取、比较 (这也是种操作)和写入这三个操作,通过硬件命令保证了原子性,运行速度快。虽然CAS也包含了多个操作,但其的运算是固定的(就是个比较),这样的锁定性能开销很小。
从内存领域来说这是乐观锁,因为它在对共享变量更新之前会先比较当前值是否与更新前的值一致,如果是,则更新,如果不是,则无限循环执行(称为自旋),直到当前值与更新前的值一致为止,才执行更新。
简单的来说,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V。这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而Synchronized是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。
下面来看一下AtomicInteger是如何利用CAS实现原子性操作的。
private volatile int value;
首先声明了一个volatile变量value,我们知道volatile保证了变量的内存可见性,也就是所有工作线程中同一时刻都可以得到一致的值。
比较并设置,这里利用Unsafe类的JNI方法实现,使用CAS指令,可以保证读-改-写是一个原子操作。compareAndSwapInt有4个参数:
- this - 当前AtomicInteger对象,
- Offset - value属性在内存中的位置(需要强调的是不是value值在内存中的位置),
- expect - 预期值,
- update - 新值,
根据上面的CAS操作过程,当内存中的value值等于expect值时,则将内存中的value值更新为update值,并返回true,否则返回false。在这里我们有必要对Unsafe有一个简单点的认识,从名字上来看,不安全,确实,这个类是用于执行低级别的、不安全操作的方法集合,这个类中的方法大部分是对内存的直接操作,所以不安全,但当我们使用反射、并发包时,都间接的用到了Unsafe。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersiOnUID= 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
// 反射获取value属性,获取其在内存中的位置
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
public final boolean compareAndSet(int expect, int update)
{
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// 其它代码略
}
volatile可以保证可见性,但不能保证原子性,CAS操作虽然是原子性,但保证的是比较&替换的原子性,那么,AutomicInteger如何实现原子性呢?——二者结合即可!
如下代码(摘录自AutomicInteger类),采用了循环设置的策略:
- 首先读取主内存中value 的当前值 pre;
- 基于当前值 pre 进行操作(可以是很复杂的操作,不必保证原子性),得到新的值 next;
- 基于CAS操作,尝试将新值 next 写入主内存;
- 由于其它线程可能已经先行操作了主内存中的共享变量value,造成value与pre不相等,那么CAS操作将失败,返回false,循环条件成立,将继续尝试,直到成功;
/**
* Gets the current value.
*
* @return the current value
*/
public final int get() {
return value;
}
/**
* Atomically updates the current value with the results of
* applying the given function, returning the previous value. The
* function should be side-effect-free, since it may be re-applied
* when attempted updates fail due to contention among threads.
*
* @param updateFunction a side-effect-free function
* @return the previous value
* @since 1.8
*/
public final int getAndUpdate(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return prev;
}
/**
* Atomically updates the current value with the results of
* applying the given function, returning the updated value. The
* function should be side-effect-free, since it may be re-applied
* when attempted updates fail due to contention among threads.
*
* @param updateFunction a side-effect-free function
* @return the updated value
* @since 1.8
*/
public final int updateAndGet(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return next;
}