在前文中我们描述过,物理机计算机的数据缓存不一致的时候,我们一般采用两种方式来处理。一,通过总线加锁的形式,二,通过缓存一致性协议来操作。
体现缓存一致性的正是CAS(Compare-and-Swap)操作,CAS操作在整个Java并发框架中起着非常重要的作用
- 对于物理计算机中的缓存锁,在Java中是使用CAS操作来实现的。
- CAS操作的实质就是一个 [比较+替换] 的赋值过程, 通过(内存地址V,旧的预期值A,即将要更新的目标值B)的方式保证同步的一致性
- CAS操作的实质在于物理设备开放了一些指令,对于操作和冲突检测这两个步骤原子性支持
- CAS操作中会出现三个问题,ABA问题。循环时间开销太大,只能保证一个共享变量的原子操作。
一、物理计算机的缓存锁
在前文中我们提及,在物理机计算机中当处理器中数据缓存不一致的时候,一般采用总线锁。总线锁实质是把CPU和内存之前的通信锁住了,那么在锁定期间,其他的处理器是不能操作 “主内存中其他内存地址的数据”。所以总线锁的开销比较大
随着技术的进步,现在计算机已经采用了缓存锁来替代总线锁来进行性能的优化。那么什么是缓存锁呢?
【缓存锁的原理】
我们都知道在CPU数据处理中,频繁使用的内存会缓存在处理器的L1、L2和L3高速缓存里,那么数据的操作都在处理器内部缓存中进行,并不需要声明总线锁
那么,在目前的处理器中可以使用“缓存锁定”的方式来处理数据不一致的情况,这里所谓的“缓存锁定”是指:
1.内存区域如果被缓存在处理器的缓存中,并且在操作期间被锁定,那么当它执行锁操作会写到内存时,处理器并不会像锁总线的那样声明LOCK#信号,而是修改其对应的内存地址
2. 最重要的是其 允许缓存一致性来保证数据的一致性
缓存一致性核心思想:在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议。
- 每个处理通过嗅探在总线上传播的数据来检查自己的缓存的值是不是过期了
- 当处理器发现自己缓存的数据对应的内存地址被修改,就会将当前处理器缓存的数据处理为无效。
- 当处理器对这个数据进行修改的操作的时候,会重新从系统内存中把数据读到处理器缓存中。
1.1 缓存锁与CAS(Compare-and-Swap)的关系
为了实现缓存锁,在物理计算机中,Intel处理器提供了很多Lock前缀的指令(注意是带Lock前缀,前缀,前缀),被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。例如,位测试和修改指令:BTS、BTR、BTC;交换指令XADD、CMPXCHG,以及其他一些操作数和逻辑指令(如ADD、OR)等。
不同的处理器实现缓存锁的指令不同,在sparc-TSO使用casa指令,而在ARM和PowerPc架构下,则需要使用一对ldrex/strex指令。
Java作为一个可以跨平台运行的语言,它势别是将这些不同的指令进行了封装以屏蔽用户对于底层差异性的感知。在Java语言中,涉及到缓存锁的操作就是CAS操作,该操作内部会最终调用不同处理器下的缓存锁Lock指令
二、Java世界的CAS操作(Compare-and-Swap)
CAS是Compare-and-swap(比较与替换)的简写,是一种有名的无锁算法.
CAS指令需要三个操作数,分别是
- 内存地址(在Java内存模型中可以简单理解为主内存中变量的内存地址)
- 旧值(在Java内存模型中,可以理解工作内存中缓存的主内存的变量的值)
- 新值
CAS操作执行时,当且仅当主内存对应的值等于旧值时,处理器用新值去更新旧值,否则它就不执行更新。但是无论是否更新了主内存中的值,都会返回旧值,上述的处理过程是一个原子操作。
CAS操作并不是代码级别的互斥同步,而是直接借助物理设备实现的操作和冲突检测这两个步骤。在JDK 1.5之后,Java程序中才可以使用CAS操作,该操作由sun.misc.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供,虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果就是一条平台相关的处理器CAS指令,没有方法调用的过程,或者可以认为是无条件内联进去了
对于概念类的东西,大家理解起来比较困难,这里简单举个例子。 针对一个共享变量Int,我们使用20个线程对它进行a++自增操作10次
public class AtomicTest{
//public static volatile int race=0;//1
public static AtomicInteger race=new AtomicInteger(0);//2
public static void increase(){
race.incrementAndGet();
}
//public static synchronized void increase(){
// race++;
//}
private static final int THREADS_COUNT=20;
public static void main(String[]args)throws Exception{
Thread[]threads=new Thread[THREADS_COUNT];
for(int i=0;i<THREADS_COUNT;i++){
threads[i]=new Thread(new Runnable(){
@Override
public void run(){
for(int i=0;i<10000;i++){
increase();
}
}
});
threads[i].start();
}
//所有线程都执行完毕后,打印结果
while(Thread.activeCount()>1) Thread.yield();//yield在于阻塞当前线程
System.out.println(race);
}
}
- 如果是1处使用的代码volatile,则结果并不是200. 这是由于 volatile只能保证可见性并不能保证 a++自增操作的原子性,多线程在读写时可能出现覆盖
- 把“race++”操作或increase()方法用synchronized同步块包裹起来当然是一个办法,使用synchronized修饰后,increase方法变成了一个原子操作,因此是肯定能得到正确的结果。但这里我们暂时不关注这方面
- 使用 AtomicInteger 代替int后,程序输出了正确的结果 200,一切都要归功于incrementAndGet()方法的原子性。
AtomicInteger保证了线程安全的奥秘就在于 CAS操作,我们来看incrementAndGet()
:
public final int incrementAndGet(){
for(;;){
int current=get();
int next=current+1;
if(compareAndSet(current,next))
return next;
}
}
incrementAndGet()方法在一个无限循环中(也就是CAS的自旋),不断尝试将一个比当前值大1的新值赋给自己。如果失败了,那说明在执行“获取-设置”操作的时候值已经有了修改,于是再次循环进行下一次操作,直到设置成功为止。
【CAS(Compare-and-Swap)简介】
在上图中,分别有两条线程A与B,假设线程A优先与线程B执行AtomicInteger a++操作,则
- 对于A线程:线程A工作内存缓存a的值为10,主内存中的a的值也为10,这个时候如果进行CAS操作 cas(10,11)
- 会将工作内存与主内存中的a的值进行对比
- 发现是相等的,则执行a++操作运算
- 将执行结果也就是11同步到主内存中,这个时候主内存中的值为11
- 对于B线程:工作内存中缓存的a的值为8,主内存a的值为11,这个时候如果进行CAS操作 cas(8,9)
- 会将工作内存与主内存中的a的值进行对比
- 发现不相等,据缓存一致性原则。会重新去主内存读取a的值(11),此时线程B中工作内存中缓存的a的值为11,
- 执行a++运算后,这个时候如果进行CAS操作 cas(10,12)
- 将执行结果也就是12同步到主内存中,这个时候主内存中的值为12
需要特别注意的是: CAS操作其实就是一次赋值过程,只不过这个赋值过程前需要校验正确性,不正确则直接关闭。上述示例中的,再次读取内存中的值并再次执行运算,其实已经不属于CAS操作的范畴,它的实现可以参看后边的AtomicXX
三、CAS在Java中的实现
我们继续上文的AtomicInteger#incrementAndGet()
,其中的compareAndSet()
方法的实现很简单,只有一行代码。这里涉及到两个重要的对象,一个是 unsafe ,一个是 valueOffset。
private static final sun.misc.Unsafe unsafe = sun.misc.Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (ReflectiveOperationException e) {
throw new Error(e);
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, VALUE, expect, update);
}
- 什么是unsafe呢?Java语言不像C,C++那样可以直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。
- 至于valueOffset对象,是通过unsafe.objectFieldOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单地把valueOffset理解为value变量的内存地址。
在java中,我们主要分析Unsafe类,因为所有的CAS操作都是它来实现的,在Unsafe类中这些方法也都是native方法
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
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);
由于Unsafe类不是提供给用户程序调用的类(Unsafe.getUnsafe()的代码中限制了只有启动类加载器(Bootstrap ClassLoader)加载的Class才能访问它)。因此,如果不采用反射手段,我们只能通过其他的Java API来间接使用它,如J.U.C包里面的整数原子类,其中的compareAndSet()和getAndIncrement()等方法都使用了Unsafe类的CAS操作。
这里我们就使用compareAndSwapInt来讲解,具体代码如下:
private static final jdk.internal.misc.Unsafe theInternalUnsafe = jdk.internal.misc.Unsafe.getUnsafe();
/**
* 第一个参数object是当前对象
* 第二个参数offest表示该变量在内存中的偏移地址(CAS底层是根据内存偏移位置来获取的)
* 第三个参数expected为旧值
* 第四个参数x为新值。
*/
public final boolean compareAndSwapInt(Object o, long offset,
int expected,
int x) {
return theInternalUnsafe.compareAndSetInt(o, offset, expected, x);
}
//继续查看theInternalUnsafe下的compareAndSetInt()方法。也是一个本地方法。
//这里具体的本地方法是在hotspot下的unsafe.cpp类具体实现的。
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
//compareAndSetInt调用unsafe.cpp中的JNI方法具体实现如下:
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {
oop p = JNIHandles::resolve(obj);
if (p == NULL) {
volatile jint* addr = (volatile jint*)index_oop_from_field_offset_long(p, offset);
return RawAccess<>::atomic_cmpxchg(x, addr, e) == e;
} else {
assert_field_offset_sane(p, offset);
return HeapAccess<>::atomic_cmpxchg_at(x, p, (ptrdiff_t)offset, e) == e;
}
} UNSAFE_END
unsafe.cpp最终会调用atomic.cpp, 而atomic.cpp会根据不同的处理调用不同的处理器指令,这里我们还是以Intel的处理器为例,atomic.cpp最终会调用atomic_windows_x86.cpp中的operator()方法。
template<>
template<typename T>
/**
*第一个参数exchange_value为新值
*第二个参数volatile* dest为变量内存地址(也就是主内存中变量地址)
*第三个参数compare_value为旧值(也就是工作内存中缓存的变量值)。
*其中在方法中,asm是C++中的关键字,主要作用为启动内联汇编,同时其能写在任何C++合法语句之处。它不能单独出现,必须接汇编指令、一组被大括号包含的指令或一对空括号。
*/
inline T Atomic::PlatformCmpxchg<4>::operator()(T exchange_value,
T volatile* dest,
T compare_value,
atomic_memory_order order) const {
STATIC_ASSERT(4 == sizeof(T));
// alternative for InterlockedCompareExchange
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
lock cmpxchg dword ptr [edx], ecx
}
}
那么针对于operrator中的汇编语句块进行分析,要内容分为四个部分(这里我们就把edx,ecx,eax当做存储数据的容器):
- mov edx, dest 将变量的内存地址赋值到edx中。
- mov ecx, exchange_value 将新值赋值到ecx中。
- mov eax,compare_value 将旧值赋值到eax中。
- lock cmpxchg dword ptr [edx], ecx 如果主内存中的值与旧值(也就是工作内存中缓存的变量值)不同,那么工作内存中的缓存的变量值(也就是旧值)就更新为主内存中的值。如果相同。那么主内存中的值就更新为最新的值。
- dword汇编指令 dword ptr [edx],简单来说,就是获取edx中内存地址中的具体的数据值
- cmpxchg汇编指令 格式如下:cmpxchg [第一操作数],[第二个操作数]。cmpxchg汇编指令: 主要操作逻辑是比较eax与第一操作数的值,如果相等,那么第二操作数的值装载到第一操作数,如果不相等,那么第一操作数的值装载到eax中
- lock汇编指令
- 在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低lock前缀指令的执行开销。
- 禁止该指令与前面和后面的读写指令重排序。
- 把写缓冲区的所有数据刷新到内存中。额外提一句。上面的第2点和第3点所具有的内存屏障效果,保证了CAS同时具有volatile读和volatile写的内存语义