什么是CAS
cas是compareandswap的简称,语意理解就是比较并更新,简单来说:从某一内存上取值V,和预期值A进行比较,如果内存值V和预期值A的结果相等,那么我们就把新值B更新到内存,如果不相等,那么就重复上述操作直到成功为止。
CAS有哪些角色
-
期望值
-
最新值
-
修改值
实现的步骤
-
首先获取到锁
-
获取到最新值,将最新值与期望值进行比较
-
如果期望值和最新值是一样的,那么就刷新最新值为修改值
-
如果期望值和最新值不是一样的,那么就返回false.
Java代码实现
假设我们有一个需求,某个类维护了一个成员变量,100个线程同时为这个成员变量进行加1,每个线程执行10次.那么最后一定是100次.
class CASdemo {
private static int count = 0;
public static void plus() {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++; }
public static void main(String[] args) {
long l = System.currentTimeMillis();
LongStream.range(0,100).parallel().forEach( i -> { LongStream.range(0,10).forEach(j -> CASdemo.plus());});
System.out.println("消耗的时间是:" + (System.currentTimeMillis() - l) + " ,count的值是" + CASdemo.count );
}
}
结果: 消耗的时间是:734 ,count的值是924
但是多线程并发情况下,同时去操作共享的变量,一定会造成线程安全的问题,最终导致值不准确.
原因是count++这一行代码在java里面不是原子操作,实际上这行代码分为三行.
int i = count;
i = i + 1;
count = i; // 这一步就是线程不安全的
- 修改方案1,将plus方法修改为同步方法
public synchronized static void plus()
结果多线程变成了串行方法,性能急剧下降,但是结果正确,结果是: 消耗的时间是:5243 ,count的值是1000
- 既然count++是分为三步,最后一步是线程不安全的,那么我就将最后一步进行compareAndSwap操作
class CASdemo {
private static int count = 0;
public static void plus() {
int exceptCount, modifyCount;
do {
exceptCount = count;
modifyCount = count + 1;
} while (!compareAndSwap(exceptCount, modifyCount));
}
// 1. 加锁 2. 获取最新值并比较 3. 相同则刷新 4. 不同则重复
public synchronized static boolean compareAndSwap(int exceptCount, int modifyCount) {
if (exceptCount == count) {
count = modifyCount;
return true;
}
return false;
}
public static void main(String[] args) {
long l = System.currentTimeMillis();
LongStream.range(0, 100).parallel().forEach(i -> {
LongStream.range(0, 10).forEach(j -> CASdemo.plus());
});
System.out.println("消耗的时间是:" + (System.currentTimeMillis() - l) + " ,count的值是" + CASdemo.count);
}
}
结果是: 消耗的时间是:56 ,count的值是1000
- 直接使用原子类AtomicInteger
class CASdemo {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
long l = System.currentTimeMillis();
LongStream.range(0, 100).parallel().forEach(i -> {LongStream.range(0, 10).forEach(j -> count.incrementAndGet());});
System.out.println("消耗的时间是:" + (System.currentTimeMillis() - l) + " ,count的值是" + CASdemo.count);
}
}
Java底层如何实现的
在AtomicInteger里面实际上是调用了sum.misc.Unsafe类.这个类里面有很多的cas方法
但是这个方法一般不给普通的开发者提供实例化的对象.
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);
这些方法都是采用native方法,本质上其实都是JNI,java native interface.就是Java调用了的C语言方法来实现的.
CAS有什么缺陷
CAS遇到的问题就是ABA问题.
假设count的值本来是A.
有两个线程,线程1进行 compareAndSwap 时候,线程2抢到了执行权,然后将A的值改成B,又将B的值改回A,那么线程1CAS操作是可以成功的.
想要解决ABA问题的方法就是记录版本号,同时比对版本号.
CAS和加锁处理并发哪个好
如果对版本要求不高的话,就可以进行CAS加锁的方式来进行,这样性能会比加锁好.
如果是对历史版本记录要求比较高,比如银行的操作,那么就采用加锁的方式来处理并发.
网友评论