CAS机制的原理解读

一. CAS是什么?

1、CAS的全拼是:compare and swap,中文意思是:比较和交换。CAS包含3个操作数值【内存位置(也叫偏移量)V、预期值A、新值B】,根据偏移量找到预期值做对比,如果值相等,那么用新值覆盖;如果和预期值不相等,那么该线程发生自旋(实质是循环获取对比);
2、CAS类似于乐观锁
【乐观锁】:用某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
【悲观锁】:悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。

二. CAS解决了什么问题? 如何解决的?

1.首先来看一个代码示例:
package com.nipx.demo.CAS;
public class ThreadTest1 {
    public static void main(String[] args) throws InterruptedException {
        final Counter counter = new Counter();
        /**
         * 开启10个线程,每个线程循环10000次+1操作
         * 等待20秒之后,输出值
         * 预期应该是 100000
         */
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        counter.add();
                    }
                    System.out.println("done.....................");
                }
            }).start();
        }
        Thread.sleep(20000L);
        System.out.println(counter.getI());
    }
}

两次结果为:
在这里插入图片描述
在这里插入图片描述
由此可见,它和我们预期的值100000 并不一样,为什么呢?请往下看:
首先把Counter这个类反编译,得到字节码指令文件,我们只需看2、5、6、7行
2:getfield:获取资源,获取堆内存(线程共享内存)中Counter对象的 i 值
5:iconst_1:把 i=1 这个值压入到栈顶
6:iadd: 把栈内的数值相加之后从新放入栈顶
7:putfield:把值从新写入到内存
【注意1:注意2,5,6,7序号,下面文章会频繁用到】
【注意2:看不懂的可以参照字节码对照表来看】
地址 :https://www.cnblogs.com/longjee/p/8675771.html

重点来了,如果两条线程同时getfield数值 i=0 ,线程1执行到putfield的时候,此时堆内存中的Counter的 i 值已经变成了 1,但是线程2这个时候正在执行 iconst_1 或者 iadd ,iadd执行之后 i 同样+1操作之后变成了1,线程2也把 i=1 写入内存,这个时候就造成了2条线程其中有一条是无效操作,所有就会出现它和我们预期的值 100000 不一样

在这里插入图片描述
了解多线程会出现原子性问题之后,我们进入正题CAS:
1、原子性操作可以是多个步骤,也可以是一个步骤,但是顺序不能被打乱,也不可以被切割的只执行其中一部分
2、将整个操作视为是一个整体,资源在操作中始终保持一致,这个是原子性的核心特征

【结合我们的例子来理解,我们的例子就是多个步骤,顺序没有被打乱(2,5,6,7),但是却被切割了,相当于线程1把线程2的第二步之后全部切割,数字在初始的判断之后的操作都失效了】
首先在操作系统对我们的内存条进行访问的时候,CAS保证了我们每一次只能进行一个CAS中的S(交换)操作(硬件级别控制,我们不必纠结这个)。结合上面示例来说明,我们只要保证线程不被其他线程切割就ok了,因此我们需要保证2,5,6,7是一整个资源。CAS是这么做的,它在进行swap交换之前,首先找到【预期值A】进行compare比较,如果发现这个值一样,那么进行swap;如果发现这个值不一样,那么我就不写进去。
【例如,线程1操作完之后 i=0 变成了i=1,线程2在swap的时候发现了自己的值是0,而内存中是1,那么就不进行swap了】
不写进去怎么办呢?也不能丢弃,因为我们要保证每个线程都是执行成功的。所以CAS不成功的就会发生自旋(循环调用,再来一遍2,5,6,7),先取到内存条的初始值 1(这个值已经被线程1修改了),然后再进行比较compare,如果相等则swap交换。

但是这个CAS是操作系统的一些指令,我们怎么java程序无法去和操作系统做交互,所以jvm帮助我们封装了一些方法也可以说是接口,来方便去访问操作系统;

三. CAS如何优雅的使用?

1、java中的sun.misc.Unsafe类,提供了compareAndSwapInt()和compareAndSwapLong()等一些方法来实现了CAS.
咱们自己写一个CAS的方法来验证一下。
代码如下:

package com.nipx.demo.CAS;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class CounterUnsafe {
    volatile int i = 0;
    private static Unsafe unsafe = null;
    //偏移量
    private static long valueOffset;
    static {
        //此方法不能用,JDK不让别人用,只让它自己用
        //unsafe = Unsafe.getUnsafe();
        try {
            //正确的方法是用反射来调用
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            /**将此对象的{@code accessible}标志设置为指定的布尔值.
             *{@code true}表示反射的对象应该禁止Java语言访问检查何时使用.
             *{@code false}表示反映的对象应该强制执行Java语言访问检查。*/
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
            //根据反射拿到CounterUnsafe类(本类)中的i字段的偏移量
            Field field = CounterUnsafe.class.getDeclaredField("i");
            valueOffset = unsafe.objectFieldOffset(field);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
    public void add() {
        while (true) {
            //拿到旧值
            int current = unsafe.getIntVolatile(this, valueOffset);
            //通过CAS来修改i值
            boolean b = unsafe.compareAndSwapInt(this, valueOffset, current, current + 1);
            //如果失败,发生自旋,如果成功,跳出循环
            if (b) {
                break;
            }
        }
    }
    public int getI() {
        return i;
    }
    public void setI(int i) {
        this.i = i;
    }
}

运行结果:
在这里插入图片描述
2、JUC中为我们提供的CAS工具类

package com.nipx.demo.CAS;
import java.util.concurrent.atomic.AtomicInteger;
public class CounterAtomic {
    //相当于i的初始值
    AtomicInteger atomicInteger = new AtomicInteger(0);
    public void add() {
        atomicInteger.incrementAndGet();
    }
}

效果是一样的,也是100000,结果就不粘出来了。
当然也有其他的工具类例如:
计数器:DoubleAdder、LongAdder
更新器:DoubleAccumulator、LongAccumulator

代码测试:

package com.nipx.demo.CAS;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;
public class AdderTest {
    public static void main(String[] args) throws InterruptedException {
        AdderTest test = new AdderTest();
        System.out.println("testAtomic:"+test.testAtomic());
        System.out.println("testLongAdder:"+test.testLongAdder());
        System.out.println("testLongAccumlator:"+test.testLongAccumlator());
    }
    public long testAtomic() throws InterruptedException {
        AtomicLong atomicLong = new AtomicLong(0);
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                long satrtTime = System.currentTimeMillis();
                @Override
                public void run() {
                    while (System.currentTimeMillis() - satrtTime < 2000L) {
                        atomicLong.incrementAndGet();
                    }
                }
            }).start();
        }
        Thread.sleep(3000L);
        return atomicLong.get();
    }

    /**
     * 计数器 (高并发下频繁更新,低频繁的读取,效果会更好)
     * 采用分段思想,分成多个操作单元,每个线程更新不通单元,汇总时采用sum()
     */
    public long testLongAdder() throws InterruptedException {
        LongAdder longAdder = new LongAdder();
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                long satrtTime = System.currentTimeMillis();
                @Override
                public void run() {
                    while (System.currentTimeMillis() - satrtTime < 2000L) {
                        longAdder.increment();
                    }
                }
            }).start();
        }
        Thread.sleep(3000L);
        return longAdder.sum();
    }

    //更新器
    public long testLongAccumlator() throws InterruptedException {
        LongAccumulator longAccumulator = new LongAccumulator((x, y) -> {
            return x + y;
        }, 0L);
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                long satrtTime = System.currentTimeMillis();
                @Override
                public void run() {
                    while (System.currentTimeMillis() - satrtTime < 2000L) {
                        longAccumulator.accumulate(1);
                    }
                }
            }).start();
        }
        Thread.sleep(3000L);
        return longAccumulator.get();
    }
}

测试结果:
testAtomic:90958445
testLongAdder:782979116
testLongAccumlator:748797185
可以看到运行2秒的结果LongAdder和LongAccumlator要比Atomic多出一个量级的处理

四. CAS的三个问题?

1、循环CAS,自旋的实现让所有的线程都处于高频运行,来争抢CPU执行时间的状态,如果长时间的操作不成功,则会带来很大的CPU损耗。
答案:并发很高的情况下,并发很高的情况下,并发很高的情况下,线程状态如果是blocked阻塞的话,CPU不会对它进行调度,但是CAS的线程状态一直是runnable状态,CPU会不停的对它进行调度,所以会损耗。

2、针对单个变量的操作,不能用于多个变量来实现原子操作
答案:一个CAS只能修改一个变量,例如i=i+1;但是如果同时修改多个变量,CAS时做不到的,例如i=I+1,b=b+1。

3、ABA问题
产生的原因,例如:
有两个线程同时修改一个资源i=0,在线程1操作CAS(i=0+1)成功的时候,这个时候线程2将要操作CAS,但是这个时候线程1又把 i 从 1 改成了0 ,这个时候线程2 会拿预期值进行比较后交换,但是线程2只关心值,并不关心 i 的指向,因为这个 i 虽然都是 1 但是第二个 1 已经和第一个 1 完全不一样了。

解决方法:
AtomicStampedReference类,做法就是添加一个版本号,做2个比较。第一比较版本号,第二比较值。

五. 线程安全的概念?

1、竞态条件:如果程序运行的顺序会改变最终结果,就说明存在竞态条件。原子性可能带来竞态条件。

2、本质:基于某种可能失效的观察结果做出判断或者执行了计算。

3、临界区:存在竞态条件的代码区叫做临界区。

4、导致线程安全的2个要素:原子性、可见性(volatile)

5、资源竞争中的资源是指什么?
—>进程内的共享的内存区域,例如:堆内存,方法区等
—>进程外的资源: db、redis等

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值