日常记录——多线程与高并发—CAS概念、原理、问题、CAS和synchronized比较

一、概念

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执行效率越高。(和电脑、服务器配置也有关系,个人测试数据)

猜你喜欢

转载自blog.csdn.net/weixin_43001336/article/details/107008187