一、概念
CAS:Compare and Swap(比较并且替换),jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。jdk 11 后改为 weak Compare and Set(比较并设定,weak应该是标记为弱引用,用作GC),cas操作为cpu原语支持,不需要担心在cas过程中产生线程竞争问题。
二、原理
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。继续进行自旋,等待符合条件,
注意:自旋占用cpu资源
结合juc下的AtomicInteger代码来看(jdk1.8版本):
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
伪代码就是:
do{
备份旧数据;
基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ));
再追寻使用的就是可以直接操作内存unsafe类的方法,借助c直接操作内存:
说白了就是,cas(要改的值,预期内存值,修改后的值),当且仅当要改的值和内存预期值相等时,才会将内存值修改。每次失败会重新获取要改值和预期值。
举例:当一个线程想修改值的时候,我期望你的值时0,如果你是1那么说明我这线程的缓存值不对,我会更新我的要改的值和预期值,当我的要改的值是1,内存期望值也是1,那么我就将我的修改后的值2,更新到内存。不用担心更新过程中,其他线程更新的问题,cas是cpu原语支持的,保证原子性。
三、问题
1.ABA问题:当两个线程访问一个变量时候,如果A线程的内存期望值,和其他线程修改后的值是一样的怎么办?A线程的期望值是1,B线程将1变成了2,C线程将2变成了1,那么A线程的CAS还是会继续执行,但是实际上期望值已经变了,对于基本数据类型看不出变化,要是引用类型的对象呢?就比如你和你前女友复合,在复合前他经历了2个男友,在你的认识里还是那个她,因为你不知道她经历了什么,但是实际上她的生活轨迹里已经多了2个男(话糙理不糙,勿喷,为了理解)。
怎么解决这种问题呢:加版本号呗,乐观锁的实现方式么,期望值还是我的期望值,但是经历一次操作版本号加一,不就知道是不是我的期望值了么。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。
2.CPU消耗问题:如果并发数量过多,就会导致很多线程在自旋等待其他线程操作完毕,自旋是消耗cpu资源的,所以什么场景下使用cas是需要思考的。
3.只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
四、CAS和synchronized比较
CAS:乐观锁,并发访问量小时候使用。
synchronized:由于jdk更新优化,以及锁升级的概念,在并发小的情况下仍有较好的表现,但并发量高的情况下,表现比CAS效果更好。
测试代码:
package com.company.CASs;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public class casTest {
volatile static boolean flag = true;
public static void main(String[] args) {
testSynchronized testSynchorized = new testSynchronized();
testCas testCas = new testCas();
AtomicLong times = new AtomicLong();
for (int i=0;i<8;i++){
new Thread(() ->{
long begin = System.currentTimeMillis();
while(true)
{
if(testSynchorized.getCount() >= 100000000)
{
flag = false;
break;
} else{
testSynchorized.add();
}
}
long end = System.currentTimeMillis();
long time = end - begin;
times.addAndGet(time);
}).start();
}
while(flag){ }
System.out.println("总时间:"+times);
}
}
class testSynchronized{
private volatile int count = 0;
public synchronized int getCount(){
return count ;
}
public synchronized void add(){
if(count<100000000){
count++;
}
}
}
class testCas {
private volatile AtomicInteger count = new AtomicInteger(0);
public int getCount(){
return count.get();
}
public void add(){
if(count.get()<100000000){
count.addAndGet(1);
}
}
}
只需在代码内更改测试对象和线程数即可,
电脑配置:
加到100000000,8线程效果:
cas:耗时22020毫秒;
synchronized耗时:29800毫秒;
并发为16时:
cas耗时:33764毫秒
synchronized耗时:38131毫秒;
并发数为32时:
cas耗时:61865毫秒;
synchronized耗时:29329毫秒;
测试数据汇总:
并发线程数 | 累积和 | cas耗时(ms) | synchronized耗时(ms) |
---|---|---|---|
8 | 100000000 | 22020 | 29800 |
16 | 100000000 | 33764 | 38131 |
32 | 100000000 | 61865 | 29323 |
结论:通过数据可见,相同并发数情况下,并发数越低,cas执行效率越高,并发数越高,synchronized执行效率越高。(和电脑、服务器配置也有关系,个人测试数据)