CAS即Compare And Swap,我们称为比较再交换。
在Java中,并发情况下,我们可以使用synchronized来实现锁。在jdk1.5之前,synchronized为悲观锁,后面由于ReentrantLock的无锁操作,synchronized也进行了改版,性能反而比ReentrantLock的好一点,但核心原理和ReentrantLock相关无几,其中都有使用CAS算法。
CAS是用在多线程环境下,要真正理解CAS,我们还需要了解一点关于CPU的知识:线程是CPU调度的最小单元,线程设计的目的是为了更充分的利用计算机处理的能力。CPU还需要与内存交互,如读取去处数据、存储结果,这个I/O操作很难消除。由于存储设备与处理器的运算速度差距非常大,所以计算机系统都会增加一层读写速度尽可能接近处理器处理速度的高速缓存来作为内存和处理器之间的缓冲【将需要使用的数据复制到缓冲中,让运算能快速进行,当运算结束后再从缓冲同步到内存中】。
CPU有L1、L2、L3三级缓存,大致结构如下:
L1和L2缓存为各个CPU独有,而有了高速缓存以后,每个CPU的处理过程是先将需要用到的数据缓存在CPU高速缓存中,在CPU计算时,直接从高速缓存中读取数据并且在计算完成之后写入到缓存中,运算完成后再把缓存中的数据同步到主内存。
在多核心CPU环境下,每个线程可能会运行在不同的CPU核心内,并且每个线程拥有自己的高速缓存。同一份数据可能会被缓存到多个CPU中,由于每个CPU的缓存是独立的,在运行的不同线程看到同一份内存的缓存值不一样就会存在缓存不一致的问题。CPU层面提供两种解决办法:总线锁和缓存锁。
对于缓存不一致的问题,我在讲volatile关键字的时候再讲。我们大致了解了CPU计算的过程,一般我们在多线程环境下,都会使用锁来解决资源共享的问题。但是锁是非常消耗性能的,所以才有了CAS,CAS本身是没有锁的。
CAS算法是一种无锁算法,在CAS操作中,存在3个操作数:
-
内存值V
-
期望内存中的值A(说白了就是内存中的值,但是内存中的值可能被其它线程修改)
-
新值B
举个例子:假如内存中有一个int类型的值为1,我们要将1做自增操作。刚开始内存值等于1,期望值为1。在CPU做完计算后,如果内存中的值依旧和期望值相等,那么就证明没有被其它线程做过修改,于是将内存中的值修改为计算后的新值B。如果内存中的值不等于期望值,则证明该值被其它线程修改过,于是不做任何处理。
CPU的计算非常之快,在大部分的并发场景下,CAS效率都很高,虽然CAS失败后会继续CAS,这样如果一直失败就会一直占用CPU。但是大部分的并发场景的并发并不是特别高,所以CAS的重试次数可以接受,但是一旦并发量达到一定数量,CAS就会疯狂重试,这样反而会导致性能下降。
ABA问题
假设有一个变量A,修改为B,然后又修改为了A,实际已经修改过两次,最终结果没有变,在这种情况下,CAS还是认为值没有变,作出了错误的判断,造成了不合理的值修改操作。
对于这种问题,我们想到的办法就是增加一个版本号,记录每次修改时的版本,这样通过版本号来判断是否做过修改。在Java中,JDK提供了AtomicStampedReference,可以通过建立一个Stamp类似版本号的方式,确保CAS操作的正确性。
synchronized
下面我们通过几个示例来一步步深入CAS底层:
使用多个线程对value进行自增操作,这里有个问题,我们不能保证最后输出的value的结果是在线程都执行完成后的结果,这里使用CountDownLatch来保证线程执行完成。
package com.doaredo.test;
import java.util.concurrent.CountDownLatch;
public class Increment {
private int value;
public int get(){
return value;
}
public void increment(){
value++;
}
public static void main(String[] args) throws InterruptedException {
int threadSize = 100;
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
Increment increment = new Increment();
for (int i = 0; i < threadSize; i++) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int j = 0; j < 10000; j++) {
increment.increment();
}
} finally {
countDownLatch.countDown();
}
}
});
t1.start();
}
countDownLatch.await();
System.out.println(increment.get());
}
}
我们可以看到最后输出的结果很大概率都不是1000000。这是因为increment()方法中的value++并不是原子性操作,我们可以使用synchronized关键字加锁来保证原子性。
public synchronized void increment(){
value++;
}
CAS
我们可以很简单的通过加锁来实现线程安全的问题,但是这种方式性能低下,因此我们需要寻求效率更高的方法。也就是我们今天要了解的CAS,CAS其实就是乐观锁。看个例子:
package com.doaredo.test;
import java.util.concurrent.CountDownLatch;
public class Increment {
private int value;
public int get(){
return value;
}
public void increment(){
int expect;
while(!compareAndSwap(expect = this.value, expect + 1)){
}
}
public synchronized boolean compareAndSwap(int expect, int newValue){
if(this.value == expect){
this.value = newValue;
return true;
}
return false;
}
public static void main(String[] args) throws InterruptedException {
int threadSize = 100;
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
Increment increment = new Increment();
for (int i = 0; i < threadSize; i++) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int j = 0; j < 10000; j++) {
increment.increment();
}
} finally {
countDownLatch.countDown();
}
}
});
t1.start();
}
countDownLatch.await();
System.out.println(increment.get());
}
}
这里,我们使用compareAndSwap方法,传入一个期望值,一个新值。如果现在value的值与期望值相等,那么将新值赋值给value,返回true,否则返回false。外面使用一个死循环,直到value值与期望值相等才结束循环。
这就是简单的Cas操作原理。如果内存中的值与期望值相等,那么就修改内存中的值,如果不相等就不做操作。
我们都说CAS是无锁操作,但是我们的compareAndSwap方法上还是有synchronized加锁啊。真正的CAS实现其实是使用CPU指令cmpxchg来实现的,这个指令就是比较再替换,其本身就是原子操作,上面我们的compareAndSwap方法只是简单的来模仿cmpxchg指令的操作,为了保证原子性,所以代码上增加了synchronized关键字。
但是,我们将通过JNI以及汇编来实现CAS,从而了解JDK实现CAS的真正底层原理。
JNI实现CAS
下面我们通过JNI以及汇编来实现CAS。这里涉及到的JNI以及汇编都是比较简单入门的,同学们不要太过担心,慢慢来,你也可以的。
package com.doaredo.test;
public class Cas {
private volatile int value;
private volatile int[] arrValue;
static {
System.loadLibrary("HelloCas");
}
public Cas(int initialValue) {
this.value = initialValue;
this.arrValue = new int[]{initialValue};
}
public int get(){
return value;
}
public void increment(){
int expect;
while(!compareAndSwapInt(expect = value, expect + 1, arrValue)){
}
}
public final native boolean compareAndSwapInt(int expect, int newValue, int[] arr);
}
使用Visual Studio创建dll项目(注意:这里使用的是Win 32位操作系统,当然64位的Win系统也可以运行32位程序的,对应Jdk也要使用32位的才能正常的使用JNI,不同的操作系统所使用的汇编指令可能会不一样)
文件->新建->项目->Visual C+±>Windows桌面->Windows桌面向导->动态链接库(.dll)、空项目
右键解决方案->属性(配置生成dll的平台)
右键项目->生成
com_doaredo_test_Cas.h头文件
/* DO NOT EDIT THIS FILE - it is machine generated */
#include "jni.h"
/* Header for class com_doaredo_test_Cas */
#ifndef _Included_com_doaredo_test_Cas
#define _Included_com_doaredo_test_Cas
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_doaredo_test_Cas
* Method: getIntValue
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_com_doaredo_test_Cas_getIntValue
(JNIEnv *, jobject);
/*
* Class: com_doaredo_test_Cas
* Method: compareAndSwapInt
* Signature: (II[I)Z
*/
JNIEXPORT jboolean JNICALL Java_com_doaredo_test_Cas_compareAndSwapInt
(JNIEnv *, jobject, jint, jint);
#ifdef __cplusplus
}
#endif
#endif
class Cas
{
public:
inline static jint cmpxchg(jint compare_value, int* dest, jint exchange_value);
};
Cas.cpp文件
#include "com_doaredo_test_Cas.h"
#include <iostream>
#include <stdio.h>
using namespace std;
int value;
JNIEXPORT jint JNICALL Java_com_doaredo_test_Cas_getIntValue
(JNIEnv *env, jobject obj) {
__asm {
lock add dword ptr[esp], 0;
}
return value;
}
JNIEXPORT jboolean JNICALL Java_com_doaredo_test_Cas_compareAndSwapInt
(JNIEnv *env, jobject obj, jint compare_value, jint exchange_value) {
//jint result = Cas::cmpxchg(compare_value, &value, exchange_value);
//return result == compare_value;
return Cas::cmpxchg(compare_value, &value, exchange_value) == compare_value;
}
inline jint Cas::cmpxchg(jint compare_value, int* dest, jint exchange_value)
{
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
lock cmpxchg dword ptr[edx], ecx
}
}
生成dll文件。
配置java项目的本机库位置:指定到生成的dll文件目录。
package com.doaredo.test;
import java.util.concurrent.CountDownLatch;
public class Cas {
private int value;
static {
try {
System.loadLibrary("Cas");
} catch (Exception ex) {
throw new Error(ex);
}
}
public Cas(int initialValue) {
this.value = initialValue;
}
public void increment(){
int expect;
do{
expect = this.getIntValue();
}while(!this.compareAndSwapInt(expect, expect + 1));
}
public native int getIntValue();
public final native boolean compareAndSwapInt(int expect, int newValue);
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
int threadSize = 1000;
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
Cas cas = new Cas(0);
for (int i = 0; i < threadSize; i++) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int j = 0; j < 100000; j++) {
cas.increment();
}
} finally {
countDownLatch.countDown();
}
}
});
t1.start();
}
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println(cas.getIntValue()+"====="+(end-start));
}
}
总结
这里并没有深入介绍,我觉得涉及到的知识太多。我们只是通过自己写的一个简单的JNI程序来模拟Cas操作,其中最核心的就是cmpxchg指令,一般我们了解到这个指令基本上就可以了。
其实这里的实现原理和Jdk中的实现原理是一样的,有兴趣的同学可以去看看Jdk中这部分的源码。
cmpxchg c, ebx
拿EAX中的值与c比较
如果相等,则ebx中的值赋值给c
如果不相等,则c中的值放入到eax