Java的CAS机制

前言

CAS机制听起来很高大上,其实就可以把他理解为与synchronized并列的一种方式。我个人把他理解为是实现线程同步的另外一种方式(虽然本质上是异步访问,但是最终的结果与同步访问的结果是一样的)或者说,从微观上来看是异步的,但是从宏观上来看是同步的。就类似并发一样,微观上是每一时刻只执行一个进程,但是宏观上来看是多进程并发,一个道理。

1.为啥要用CAS机制?

在正式介绍CAS之前,不如聊聊它与synchronized的区别,或者它的特点,为啥要用CAS机制?

那就不得不提到悲观锁和乐观锁的概念了。
所谓悲观锁,就类似于synchronized这样的,多个线程访问被synchronized修饰的代码块,谁抢到锁,谁就执行。所以synchronized默认是高并发并且竞争很激烈,所以它很“悲观”。而乐观锁就恰恰相反,它认为程序的竞争不激烈,没有人会跟我竞争这个变量,所以它就很“乐观”。而CAS操作的锁就是乐观锁的典型例子。
悲观锁如synchronized效率其实是相对不高的,因为上下文切换需要耗费很多资源,而乐观锁的效率在大部分情况下还是很高的。我们看这张图
Java的CAS机制_第1张图片

lock就是说的synchronizedAtomicInteger就是乐观锁。可以看到当线程数目多于2时,CAS机制的性能一般来讲就优于synchronized了。

2.CAS原理

CAS的全称是Compare And Swap,就是比较 和 交换。这是CAS的核心。
CAS机制当中使用了3个基本操作数:内存地址(我们不用管),旧的预期值A,要修改的新值B。在正式修改变量之前,它要将预期值A于在相应内存地址的实际值进行比较,如果相等,则将新值B替换到内存地址的实际值,如果不相等,则将此时的实际值作为新的预期值,然后再循环。
什么意思,多说无益,我们举个实际点的例子吧。比如说有一个变量number,初始值为0.然后有两个线程对其进行number++操作。让我们来看一下此时CAS的工作原理
假如线程1 的操作流程是这样的
(这里在预期的操作那里搞错了,是得到新值1,预期的旧值为0)
Java的CAS机制_第2张图片

那么线程2 的操作流程就是这样的
Java的CAS机制_第3张图片
此时number完成了两次++,变为了2
整体流程也可以参考这张图
Java的CAS机制_第4张图片

3.CAS使用

CAS的核心就是compareswap 的那一段原子操作,而JDK中相关原子操作类基本都是以Atomic开头的。

使用示例

private static AtomicInteger number = new AtomicInteger(0);

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    //每个线程让number自增100次
                    for (int i = 0; i < 100; i++) {
                        //以下incrementAndGet方法就保证了number能够在CAS机制下进行++
                        number.incrementAndGet();
                        //这个方法和getAndIncrement的区别类似于C语言中++i 和 i++的区别
                    }
                }
            }).start();
        }

        try{
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(number);
}

这里就能够百分之百确定能够打印出200,所以能确定其进行了原子操作。
常用的原子操作类
Java的CAS机制_第5张图片

4.CAS存在的问题

什么东西有利必有弊,CAS机制也是如此。其缺点主要为三方面
①ABA问题
②开销问题
③只能保证一个共享变量的原子操作

  • ①ABA问题
    这个问题是针对于compare的。在之前的原理讲解中我们说过,compare的时候,如果预期的旧值与当前实际值相同,则进行swap。可是这个相同,是值结果相同,而不是这个值自始至终没有变过。就像上面那个number的例子。假如有线程3以极快的速度,将number从0改为了1又改为了0,那么线程1到compare的时候发现compare还是0,就认定compare没有被改过,但实际上是改过的。这就是ABA问题。比如我喝了别人的水,然后又给他盛满放回原处,我不跟他说,他可能就认为他的杯子没有被人动过,其实是被人动过的。
    解决:这个问题可以解决,就是设置一个版本戳,来监听对象的变化
    Java的CAS机制_第6张图片
    箭头的这两个方法就是具体实现类。其中,AtomicMarkableReferenceAtomicStampedReference的区别是:前者是监听这个对象变没变,后者是监听这个对象被动了几次
  • ②开销问题
    这是CAS一定会有的自己的缺陷,因为这个自旋,就把他想成死循环。死循环也是挺耗费资源的。如果长期不成功,CPU的开销还是很大的。
  • ③只能保证一个共享变量的原子操作
    这个也可以解决,就是通过这个类AtomicReference。把需要保证原子操作的多个共享变量封装成一个类,然后创建对象作为AtomicReference的参数就可以,改的时候直接换对象就行。
    比如这样
    在这里插入图片描述

OK,以上就是关于Java的CAS机制的分享,很多都是个人理解,有错误的话还请大家多多批评指正。

你可能感兴趣的:(Java进阶,java,并发编程,多线程)