CAS(Compare And Swap)是对一种处理器指令的称呼,中文译为:比较并交换。
它需要三个参数:内存地址V、期望的旧值A、要替换的新值B。
它要完成的功能:当且仅当内存地址V的值等于A时,将A替换为B并返回true,否则什么也不做直接返回false。
用Java代码描述,大致如下所示:
/**
* @paramv 内存地址
* @param a 期望旧值
* @param b 替换的新值
* @return
*/
boolean compareAndSwap(V v,A a,B b){
if (v == a){
v = b;
return true;
}
return false;
}
可以看到CAS是一个典型的check-and-act
操作,如果不加锁很明显它不是一个原子操作。
JUC下很多类都用到了大量的CAS操作,如:AQS、ConcurrentHashMap、atomic包下的原子变量类。
它们是如何做到在不加锁的情况下确保多线程安全的呢?
CAS操作依赖于现代CPU支持的并发原语,换句话说,整个“比较并交换”的过程CPU会保证它的原子性,执行过程中不会因为时间片用完而被中断。
因此Java语言本身是无法实现CAS操作的,需要借助JNI调用本地代码来实现。
在Java平台中,利用sun.misc.Unsafe
类来完成CAS操作,查看源码会发现大多数方法都是被native修饰的,意味这Java需要调用其他语言的代码才能实现CAS操作。
正如它的名字一样,Unsafe是一个不安全的类,使用它可以直接操作内存地址、分配堆外内存。使用Unsafe分配的内存GC是不会自动回收的,因此一旦使用不当很容易造成内存泄漏,所以JDK对Unsafe类的使用做了一些限制。
Unsafe构造方法被私有化了,不能直接new实例,提供了一个获取实例的方法:getUnsafe(),源码如下:
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
public static boolean isSystemDomainLoader(ClassLoader var0) {
return var0 == null;
}
要想拿到Unsafe实例,首先会检查调用类的类加载器是否为null,否则会抛出异常。
只有被Bootstrap ClassLoader
类加载器加载的类才符合这个条件,意味着开发者自己编写的类是无法获取Unsafe实例的,因为Java压根就没打算让开发者去用它。
虽然不能通过正规渠道去使用Unsafe,但是可以借助Java的反射机制来获取。
public class UnsafeIncr {
volatile int i = 0;//volatile能保证可见性,但是无法保证i++的原子性
void incr(){
i++;
}
public static void main(String[] args) throws InterruptedException {
UnsafeIncr incr = new UnsafeIncr();
//10线程 每个线程自增1万次 期望结果10万
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 10000; j++) {
incr.incr();
}
}).start();
}
//主线程休眠1s,等待10个线程执行结束
Thread.sleep(1000);
System.out.println(incr.i);
//输出结果几乎总是小于10万
}
}
public class CASIncr {
static final Unsafe unsafe;
static final long fieldOffset;
public int index = 0;
static {
try {
//反射获取Unsafe实例
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
//计算 index属性相对于UnsafeDemo类的内存偏移量
fieldOffset = unsafe.objectFieldOffset(CASIncr.class.getField("index"));
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
//index利用CAS完成自增操作
void incr(){
int old;
do {
//获取旧值
old = unsafe.getIntVolatile(this, fieldOffset);
//CAS操作 如果内存地址值=old,说明没有被其他线程修改,将新值替换为old+1并返回true,否则返回false再次自旋
} while (!unsafe.compareAndSwapInt(this, fieldOffset, old, old + 1));
}
}
利用CAS实现的自增操作是线程安全的,输出结果总是正确的。
加锁是保证线程安全功能最强大,也是适用范围最广的一种解决方案。
但是仅对于简单变量的读写操作来加锁,未免显得有点“杀鸡焉用牛刀”。
CAS可以看成是一种乐观锁的实现机制,假定不存在多线程竞争,如果变量没有被其他线程修改过那么直接写入成功,否则自旋进行重试,直到成功为止。
加锁(不考虑锁膨胀,指重量级锁)是一种悲观锁机制,认为肯定会发生数据冲突,通过OS级别的互斥量来保证每次最多只有一个线程进入临界区,通过挂起和唤醒线程来保证数据安全。
在单线程下,CAS的效率肯定是最高的,由于没有线程竞争,每次写入都会成功,完全不需要重试。
但是在线程竞争比较激烈的情况下,需要进行多次重试才能写入成功,反而会浪费CPU的性能。
这也就是为什么JDK6中加入“自适应自旋锁”的原因,竞争不激烈CAS自旋重试比挂起再唤醒线程的效率高,竞争激烈就直接挂起线程,避免浪费CPU的性能。
int index = 0,自增一亿次,分别使用synchronized和CAS测试,对比耗时:
public class SyncTest {
private int index = 0;
private final int count = 100000000;
private long startTime = System.currentTimeMillis();
synchronized void incr(){
index++;
if (index == count) {
System.out.println(System.currentTimeMillis() - startTime);
System.exit(1);
}
}
}
public class CASTest {
//不用JDK提供的原子类,自己实现
static final Unsafe unsafe;
static final long fieldOffset;
public int index = 0;
private final int count = 100000000;
private long startTime = System.currentTimeMillis();
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
fieldOffset = unsafe.objectFieldOffset(CASTest.class.getField("index"));
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
//直接使用unsafe.getAndAddInt()性能更好,这里为了更好的理解CAS,故采用compareAndSwapInt()
void incr(){
int oldValue;
do {
//获取当前值
oldValue = unsafe.getIntVolatile(this, fieldOffset);
//如果内存地址V的值=oldValue,则替换为(oldValue+1)并返回true,否则什么也不做直接返回false(说明值已被其他线程修改),继续自旋。
//依赖于现代CPU提供的并发原语,CPU会保证整个比较并交换的动作的原子性。
} while (!unsafe.compareAndSwapInt(this, fieldOffset, oldValue, oldValue + 1));
if (oldValue == count - 1) {
System.out.println(System.currentTimeMillis() - startTime);
System.exit(1);
}
}
}
线程数 | Sync耗时(ms) | CAS耗时(ms) |
---|---|---|
1 | 2456 | 1092 |
2 | 4183 | 5201 |
5 | 5633 | 8785 |
int index = 0,自增一亿次,分别测试CAS在不同数量线程的竞争下额外的自旋次数。
public class CAS {
static final Unsafe unsafe;
static final long fieldOffset;
public int index = 0;
private int count = 100000000;
private long startTime = System.currentTimeMillis();//开始时间
private AtomicInteger casCount = new AtomicInteger(0);//统计CAS次数的原子类
static {
try {
//反射拿到Unsafe实例、获取index属性的偏移量
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
fieldOffset = unsafe.objectFieldOffset(CASTest.class.getField("index"));
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
void incr(){
while (true) {
int old = unsafe.getIntVolatile(this, fieldOffset);
//CAS自增成功,结束自旋
if (unsafe.compareAndSwapInt(this, fieldOffset, old, old + 1)) {
break;
}else {
//统计额外的自旋次数
casCount.incrementAndGet();
}
}
//自增一亿次结束,输出额外的自旋次数和耗时信息
if (index == count) {
System.out.println("额外的自旋次数:"+casCount.get());
System.out.println("耗时:" + (System.currentTimeMillis() - startTime));
System.exit(1);
}
}
public static void main(String[] args) {
CAS cas = new CAS();
for (int i = 0; i < 5; i++) {
new Thread(()->{
while (true) {
cas.incr();
}
}).start();
}
}
}
线程数 | 额外自旋次数 | 耗时(ms) |
---|---|---|
1 | 0 | 1329 |
2 | 51905576 | 6585 |
5 | 147004339 | 11325 |
10 | 218429562 | 11438 |
单线程下,由于不存在竞争关系,每次写入都会成功,完全不需要自旋重试。
但是随着多线程竞争的激烈程度的上升,需要自旋重试的次数不断变多,性能也随之下降。
只做自增的话直接调用unsafe.getAndAddInt()可以获得更好的性能,本博客旨在帮助大家更好的理解CAS操作,遂取旧值再调用compareAndSwapInt()。