简述
CAS
(Compare And Swap)是对一种处理器指令的称呼,例如x86处理器中的cmpxchg
指令,是一种在多线程环境下实现同步功能的机制。CAS
在java.util.concurrent
的相关类中有非常广泛的应用,理解CAS是非常有必要的。
Unsafe类
Unsafe
是位于sun.misc
包下的一个类,主要提供一些用于执行底层、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java
语言底层资源操作能力方面起到了很大的作用,使Java语言拥有了类似C语言指针一样操作内存空间的能力。
如下Unsafe
源码所示,Unsafe
类为一单例实现,提供静态方法getUnsafe
获取Unsafe
实例,当且仅当调用getUnsafe
方法的类为引导类加载器所加载时才合法,否则抛出SecurityException
异常。
public final class Unsafe {
private Unsafe() {}
private static final Unsafe theUnsafe = new Unsafe();
@CallerSensitive
public static Unsafe getUnsafe() {
Class<?> caller = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}
}
上面的分析可知我们是无法直接调用getUnsafe()方法的,但可以用万能的反射来获取Unsafe
对象。
Java对象的布局
为了一会看懂打印出来出来的对象信息,先学一下对象的布局。
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头包括两部分:
- Mark Word:其是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特。
- klass pointer: 这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。 如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项 -XX:+UseCompressedOops 开启指针压缩,而hotspot虚拟机是默认开启指针压缩的,即Klass pointer在64位机器上是32位。
- 实例数据:就是类中定义的成员变量。
- 对齐填充:对齐填充并不是必然存在的,也没有什么特别的意义,他仅仅起着占位符的作用,由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
用法
下面先介绍Unsafe
类提供的几个常用方法,这几个核心方法都是native
方法,也就是说是用C/C++
语言实现的
- public native long objectFieldOffset(Field var1):返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该 Unsafe 函数中访问指定字段时使用。
- public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x):这个即
CAS
操作,比较对象o中偏移量为offset的变量的值是否与expected相等,相等则用x更新,然后返回true,否则返回false。
为了打印出对象头的信息,引入第三方依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
示例代码:
public class Person {
private int age;
private String name;
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
public class UnsafeTest {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
//Unsafe类中该变量是static的,直接传入null即可
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
//获取属性的offest
long ageOffset = unsafe.objectFieldOffset(Person.class.getDeclaredField("age"));
long nameOffset = unsafe.objectFieldOffset(Person.class.getDeclaredField("name"));
System.out.println("ageOffset:"+ageOffset);
System.out.println("nameOffset:"+nameOffset);
Person person = new Person();
System.out.println(ClassLayout.parseInstance(person).toPrintable());
//执行cas操作
unsafe.compareAndSwapInt(person,ageOffset,0,1);
unsafe.compareAndSwapObject(person,nameOffset,null,"张三");
System.out.println(person);
}
}
从上图可以看到获取的ageOffset
正好是属性age
在内存中的offset
,nameOffset
同理。同时person对象的属性也被成功修改。
原理
先看一下unsafe.cpp
的一些实现:
// unsafe.cpp
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
// atomic.cpp
unsigned Atomic::cmpxchg(unsigned int exchange_value,
volatile unsigned int* dest, unsigned int compare_value) {
assert(sizeof(unsigned int) == sizeof(jint), "more work to do");
//省略了一些代码
return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest,
(jint)compare_value);
}
下面为Atomic,cpp在Windows平台的具体实现:
// 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 前缀。lock指令保证了多核cpu之间数据的可见性,如果是单核cpu就没必要加lock了
*/
je L0
/*
* 0xF0 是 lock 前缀的机器码,这里没有使用 lock,而是直接使用了机器码的形式。
*/
_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
}
}
其实核心代码就是一条带lock
前缀的 cmpxchg
指令,即lock cmpxchg dword ptr [edx], ecx
。
Atomic类
Java从JDK 1.5
开始提供了java.util.concurrent.atomic
包,这个类是基于CAS
实现的能够保障对共享变量进行read-modify-write
更新操作的原子性和可见性的一组工具包。Atomic
包里的类基本都是使用Unsafe实现的包装类,其性能相比使用锁来实现原子性提升了许多。关于volatile
可以看这篇文章万字长文深入剖析volatile(Java)
read-modify-write更新操作:指对共享变量的更新不是一个简单的赋值操作,而是变量的新值依赖于变量的旧值,比如
count++
。
AtomicInteger 源码分析
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 1.获取Unsafe实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
//2.获取变量的offset
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//3.要操作的数据
private volatile int value;
}
因为AtomicInteger
类是在rt.jar包下面,通过Bootstarp ClassLoader
加载,所以代码1可以直接通过Unsafe.getUnsafe()
方法获取Unsafe类的实例。属性value要声明为volatile,以保证在多线程环境下变量的可见性。
//AtomicInteger.java
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
incrementAndGet()
这个方法会在原来value值的基础上加1,返回值为递增后的值,内部调用了Unsafe的getAndAddInt
方法来实现操作,第一个参数是AtomicInteger
实例的引用,第二个参数是value
变量在AtomicInteger
的偏移值,第三个值是要加的数字。
//Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
getAndAddInt
方法内部先通过getIntVolatile(o, offset)
获得期望值A,然后比较期望值A
是否与此刻内存中的值V
相等,如果相等,则将原来的值A
更新为B
,如果不相等,则再进行循环,直到期望值与内存中的值相等。