并发(七)--CAS

并发(七)–CAS

CAS的全称是Compare And Swap,即比较交换,当然还有一种说法:Compare And Set,调用原生CAS操作需要确定三个值:

  • 要更新的字段
  • 预期值
  • 新值

其中,要更新的字段(变量)有时候会被拆分成两个参数:1.实例 2.偏移地址。

之前关于i++的多线程问题,其调用如下:

AtomicInteger atomicInteger = new AtomicInteger();
public void addMyAtomic(){
	atomicInteger.getAndIncrement();
	}
public final int getAddIncrement(){//这个getAddIncrement解决了关于i++在多线程环境下线程安全的问题
	return unsafe.getAndAddInt(0:this,valueOffset,i:1);
	//这个this表示当前对象  valueOffset表示内存偏移量:内存地址
	}

那么又引出一个问题:unsafe类是什么?==>在rt.jar\sun.misc\Unsafe.class

==>CAS凭什么能报保证原子性?依靠的是底层的Unsafe类。

public class AtomicInteger extends Number implements java.io.sserializable{
	private static final long serialVersionUID =614....L;
	private static final Unsafe = Unsafe.getUnsafe();
	private static final long valueOffset;//表示内存地址偏移量
	static{
		try{
			valueOffset = unsafe.ObjectFieldOffset(AutomicInteger.class.getDeclaredField(name:"value"));
			}catch(Exception ex){throw new Enor(ex)}
			}
	private volatile int value;



}

(1)Unsafe:是CAS的核心类。由于java方法无法直接访问底层系统,需要通过本地(native)方法来访问也就是只要native修饰的方法,java是无能为力的),Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存。因为java中CAS操作的执行依赖于Unsafe类的方法。

注意:Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应的任务。

(2)**变量valueOffset:**表示该变量在内存中的偏移地址。因为Unsafe就是根据内存偏移地址获取数据的。

public final int getAndIncrement(){
	return unsafe.getAndAddInt(0:this,valueOffset,i:1)}

(3)变量value用volatile修饰,保证了多线程之间内存的可见性。

再次强调:由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

也许你看到这里,会觉得云里雾里,不知道我在说什么,没关系,继续硬着头皮看下去。

我们先来看看compareAndSet的源码。

compareAndSet源码浅析:

首先,调用这个方法需要传递两个参数,一个是预期值,一个是新值,这个预期值就是旧值(是值,不是字段),新值就是我们希望修改的值(是值,不是字段)。我们来看看这个方法的内部实现:

 public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

调用了unsafe下的compareAndSwapInt方法,除了传递了我们传到此方法的两个参数之外,又传递了两个参数,这两个参数就是我上面说的实例和偏移地址,this代表是当前类的实例,即AtomicInteger类的实例,这个偏移地址又是什么鬼呢,说的简单点,就是确定我们需要修改的字段在实例的哪个位置

知道了实例,知道了我们的需要修改的字段是在实例的哪个位置,就可以确定这个字段了。不过,这个确定的过程不是在Java中做的,而是在更底层做的。

偏移地址是在本类的静态代码块中获得的

   private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

unsafe.objectFieldOffset接收的是Field类型的参数,得到的就是对应字段的偏移地址了,这里就是获得value字段在本类,即AtomicInteger中的偏移地址。

我们在来看看value字段的定义:

 private volatile int value;

volatile是为了保证内存的可见性。

我们可以看下这两个方法的定义:

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public native long objectFieldOffset(Field var1);

这两个方法被native标记了。

我们来为compareAndSwapInt方法做一个比较形象的解释:

当我们执行compareAndSwapInt方法,传入10和100,Java会和更底层进行通信:老铁,我给你了字段的所属实例和偏移地址,你帮我看下这个字段的值是不是10,如果是10的话,你就改成100,并且返回true,如果不是的话,不用修改,返回false把

其中比较的过程就是compare,修改的值的过程就是swap,因为是把旧值替换成新值,所以我们把这样的操作称为CAS。

我们再来看看incrementAndGet的源码。

incrementAndGet源码浅析:

  public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
//这个getAndAddInt是一个难点:
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;
    }

incrementAndGet方法会调到用getAndAddInt方法,这里有三个参数:

  • var1:实例。
  • var2:偏移地址。
  • var4:需要自增的值,这里是1。
public final int getAndIncrement(){
	return unsafe.getAndAddInt(0:this,valueOffset,i:1)}

和这个对比,那么其实this就是var1,valueOffset就是var2,i:1就是var4.先get到当前的值,比较var5是否==var5,是的话就
var5+var4.若while(!true)则跳出循环,否则再do while。

(var1:AtomicInteger对象本身
var2:该对象值的引用地址
var4:需要变动的数量
var5:是通过car1,var2找出主存内存中真实的值

用该对象当前的值与var5比较,若相同就更新var5+var4,并且返回true;若不相同,继续取值然后在比较,直到更新完成。)

getAndAddInt方法内部有一个while循环,循环体内部根据实例和偏移地址获得对应的值,这里先称为A,再来看看while里面的判断内容,JDK和更底层进行通讯:嘿,我把实例和偏移地址给你,你帮我看下这个值是不是A,如果是的话,帮我修改成A+1,返回true,如果不是的话,返回false吧。

这里要思考一个问题:为什么需要while循环?

比如同时有两个线程执行到了getIntVolatile方法,拿到的值都是10,其中线程A执行native方法,修改成功,但是线程B就修改失败了啊,因为CAS操作是可以保证原子性的,所以线程B只能苦逼的再一次循环,这一次拿到的值是11,又去执行native方法,修改成功。

像这样的while循环,有一个高大上的称呼:CAS自旋。

让我们试想一下,如果现在并发真的很高很高,会出现什么事情?大量的线程在进行CAS自旋,这太浪费CPU了吧。所以在Java8之后,对原子操作类进行了一定的优化,这个我们后面再说。

可能大家对于原子操作类的底层实现,还是比较迷茫,还是不知道unsafe下面的方法到底是什么意思,毕竟刚才只是简单的读了下代码,俗话说“纸上得来终觉浅,绝知此事要躬行”,所以我们需要自己调用下unsafe下面的方法,来加深理解。

Unsafe:

Unsafe:不安全的,既然有这样的命名,说明这个类是比较危险的,Java官方也不推荐我们直接操作Unsafe类,但是毕竟现在是学习阶段,写写demo而已,只要不是发布到生产环境,又有什么关系呢?

Unsafe下面的方法还是比较多的,我们选择几个方法来看下,最终我们会利用这几个方法来完成一个demo。

  • objectFieldOffset:接收一个Field类型的数据,返回偏移地址。
  • compareAndSwapInt:比较交换,接收四个参数:实例,偏移地址,预期值,新值。
  • getIntVolatile:获得值,支持Volatile,接收两个参数:实例,偏移地址。

这三个方法在上面的源码浅析中,已经出现过了,也进行了一定的解释,这里再解释一下,就是为了加深印象,我在学习CAS的时候,也是反复的看博客,看源码,突然恍然大悟。我们需要用这三个方法来完成一个demo:写一个原子操作自增的方法,自增的值可以自定义,没错,这个方法上面我已经分析过了。下面直接放出代码:

public class MyAtomicInteger {

    private volatile int value;

    private static long offset;//偏移地址

    private static Unsafe unsafe;

    static {
        try {
            Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafeField.setAccessible(true);
            unsafe = (Unsafe) theUnsafeField.get(null);
            Field field = MyAtomicInteger.class.getDeclaredField("value");
            offset = unsafe.objectFieldOffset(field);//获得偏移地址
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void increment(int num) {
        int tempValue;
        do {
            tempValue = unsafe.getIntVolatile(this, offset);//拿到值
        } while (!unsafe.compareAndSwapInt(this, offset, tempValue, value + num));//CAS自旋
    }

    public int get() {
        return value;
    }
}
public class Main {
    public static void main(String[] args) {
        Thread[] threads = new Thread[20];
        MyAtomicInteger atomicInteger = new MyAtomicInteger();
        for (int i = 0; i < 20; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    atomicInteger.increment(1);
                }
            });
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("x=" + atomicInteger.get());
    }
}

运行结果:

在这里插入图片描述
你可能会有疑问,为什么**需要用反射来获取theUnsafe**,其实这是JDK为了保护我们,让我们无法方便的获得unsafe,如果我们和JDK一样来获得unsafe会报错:

 @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");//如果我们也以getUnsafe来获得theUnsafe,会抛出异常
        } else {
            return theUnsafe;
        }
    }

ABA问题(也就是CAS的缺点):

最主要的缺点就是ABA问题:

compareAndSet方法,上面已经写过一个demo,大家可以也试着分析下源码,我就不再分析了,我之所以要再次提到compareAndSet方法,是为了引出一个问题。

假设有三个步骤:

  • 修改150为50
  • 修改50为150
  • 修改150为90

请仔细看,这三个步骤做的事情,一个变量刚开始是150,修改成了50,后来又被修改成了150!(又改回去了),最后如果这个变量是150,再改成90。这就是CAS中ABA的问题。

第三步,判断这个值是否是150,有两种不同的需求:

  • 没错啊,虽然这个值被修改了,但是现在被改回去了啊,所以第三步的判断是成立的。
  • 不对,这个值虽然是150,但是这个值曾经被修改过,所以第三步的判断是不成立的。

针对于第二个需求,我们可以用AtomicStampedReference来解决这个问题,AtomicStampedReference支持泛型,其中有一个stamp的概念。下面直接贴出代码:

public static void main(String[] args) {
        try {
            AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(150, 0);
            Thread thread1 = new Thread(() -> {
                Integer oldValue = atomicStampedReference.getReference();
                int stamp = atomicStampedReference.getStamp();
                if (atomicStampedReference.compareAndSet(oldValue, 50, 0, stamp + 1)) {
                    System.out.println("150->50 成功:" + (stamp + 1));
                }
            });
            thread1.start();

            Thread thread2 = new Thread(() -> {
                try {
                    Thread.sleep(1000);//睡一会儿,是为了保证线程1 执行完毕
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Integer oldValue = atomicStampedReference.getReference();
                int stamp = atomicStampedReference.getStamp();
                if (atomicStampedReference.compareAndSet(oldValue, 150, stamp, stamp + 1)) {
                    System.out.println("50->150 成功:" + (stamp + 1));
                }
            });
            thread2.start();

            Thread thread3 = new Thread(() -> {
                try {
                    Thread.sleep(2000);//睡一会儿,是为了保证线程1,线程2 执行完毕
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Integer oldValue = atomicStampedReference.getReference();
                int stamp = atomicStampedReference.getStamp();
                if (atomicStampedReference.compareAndSet(oldValue, 90, 0, stamp + 1)) {
                    System.out.println("150->90 成功:" + (stamp + 1));
                }
            });
            thread3.start();

            thread1.join();
            thread2.join();
            thread3.join();
            System.out.println("现在的值是" + atomicStampedReference.getReference() + ";stamp是" + atomicStampedReference.getStamp());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。

  • 循环时间长开销很大。
  • 只能保证一个共享变量的原子操作。
  • ABA问题。

循环时间长开销很大:
我们可以看到getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。(也就是,未成功时,会一直do…while来循环

只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

什么是ABA问题?ABA问题怎么解决?
如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?

如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题ABA问题的解决思路就是使用版本号。在变量前面加上版本号,每次变量更新的时候把版本号+1,那么A->B->A就变成了1A->2B->3A。,Java1.5开始,并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

为什么要用CAS而不是用synchronized?

synchronized加锁,同一时间段内只允许一个线程来访问,一致性得到了保障,但是并发性下降。
CAS底层用到了do。。while(也就是getAndAddInt()方法),这里没有加锁,但是可以反复使用CAS比较,直到比较成功,这样既保证了一致性又提高了并发性。

下面写一个例子,来帮助理解CAS:

假设A和B两个线程同时执行getAndAddInt操作(分别跑在不同的cpu上):

1,AtomicInteger里面的value原始值为3,即主存中AtomicInteger的value为3,根据JMM模型,线程A和B各自持有一份值为3的value的副本分别到各自的工作内存。
2,线程A通过getAndAddInt(var1,var2)拿到value值为3,这时线程A被挂起
3,线程B也通过getAndAddInt(var1,var2)方法获取到value值为3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法,比较内存值也为3,成功修改内存值为4,线程B打完收工,一切ok。
4,这时候线程A恢复,执行compareAndAwapInt方法比较,发现自己手里的值数字是3,和主存中的数字4不一致,说明该值已经被其他线程抢先一步修改过了,则A线程本次修改失败,只能重新获取重新来一遍了。
5,线程A重新获取value值,因为value被volatile修饰,所以其线程对它的修改,线程A总是能够看到,线程A继续执行comapreAndSwapInt进行比较替换,直到成功。

感谢 并参考:

https://mp.weixin.qq.com/s/GPEHpzQvAXNvH0i__Hok4Q

weixin073智慧旅游平台开发微信小程序+ssm后端毕业源码案例设计 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
python017基于Python贫困生资助管理系统带vue前后端分离毕业源码案例设计 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值