欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

CAS机制

程序员文章站 2022-05-05 10:27:49
...

先来看一段代码,看一下执行结果是多少?

示例:启动两个线程,每个线程中让静态变量count循环累加100次

public class CAS {

    public static int count=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 (InterruptedException e){
                                e.printStackTrace();
                            }
                            //每个线程当中让count值自增100次
                            for(int j=0;j<100;j++){
                                    count++;
                            }
                        }
                    }
            ).start();
        }
        try{
            Thread.sleep(2000);
        }catch (InterruptedException e){
            e.printStackTrace();

        }
        System.out.println("count="+count);
    }
}

结果一定是200吗?显示是不一定的,因为是非线程安全的。

CAS机制

解决方案那??

通常情况下我们首先想到的是加synchronized同步锁,如下:

public class CAS {

    public static int count=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 (InterruptedException e){
                                e.printStackTrace();
                            }
                            //每个线程当中让count值自增100次
                            for(int j=0;j<100;j++){
                                    synchronized (CAS.class) {
                                        count++;
                                    }
                            }
                        }
                    }
            ).start();
        }
        try{
            Thread.sleep(2000);
        }catch (InterruptedException e){
            e.printStackTrace();

        }
        System.out.println("count="+count);
    }
}

结果:显然是没问题的
CAS机制

还有另外一种解决方案,都知道Java中有原子操作类,在java.util.concurrent.atomic包下,一系列以Atomic开头的包装列,例如AtomicBoolean、AtomicInteger、AtomicLong,分别用于Boolean、Integer、Long类型的原子性操作。

修改代码如下:

public class casAtomicInteger {

    public static AtomicInteger count=new AtomicInteger();

    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 (InterruptedException e){
                                e.printStackTrace();
                            }
                            //每个线程当中让count值自增100次
                            for(int j=0;j<100;j++){
                                count.incrementAndGet();
                            }
                        }
                    }
            ).start();
        }
        try{
            Thread.sleep(2000);
        }catch (InterruptedException e){
            e.printStackTrace();

        }
        System.out.println("count="+count);
    }
}

那么这两种方式有什么区别那?那种性能更好一些那?

对于这种情况来说,原子操作类性能比较好一些,为什么那?下面进行分析。

synchronized关键字会让没有得到锁资源的线程进入Blocked状态,而后在争夺到锁资源后恢复为Runnable状态,过程涉及到操作系统用户模式和内核模式的转换,代价比较高。

Atomic操作类的底层采用了CAS(Compare And Swap)机制。

详细介绍CAS:
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

要更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

例子:
CAS机制

CAS机制

CAS机制

CAS机制

CAS机制

CAS机制

CAS机制

从思想上讲,Synchronized属于悲观锁,悲观地认为程序中并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序的并发情况不那么严重,所以让线程不断去尝试更新。

适用场景

在高并发的情况下,同步锁更适合一些;

Atomic系列类以及Lock系列类的底层实现都采用了CAS机制。

CAS缺点

  1. CPU开销较大
    在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
  2. 不能保证代码块的原子性
    CAS机制保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不用Synchronized了。
  3. ABA问题
    这是CAS机制最大的问题所在。
    ABA问题,就是一个变量的值从A改成了B,又改成了A.
    遵循CAS提款机的例子:
    小灰账户余额有100元,要取50,由于提款机硬件出了点小问题,小灰取款的操作同时提交了两次,开启了两个线程,两个线程都要获取当前值100,更新为50。

正常情况下应该一个线程更新成功,另一个线程更新失败,小灰的存款只被扣一次。

CAS机制

线程1首先执行成功,把100改成50,线程2因为某种原因阻塞了,这时候,小灰妈妈刚好给小灰汇款50。
CAS机制

线程2仍然阻塞状态,线程3执行成功,把余额从50改成100。

CAS机制

线程2恢复运行,由于阻塞之前已经获得了“当前值”100,并且经过compare检测,此时存款实际值也是100,所以成功把100更新成了50。

CAS机制

换成任何一个人都不干啊,无缘无故少了50块钱。

那么ABA的问题怎么解决那???

加版本号

要做到严谨的CAS机制,我们在Compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。
CAS机制

CAS机制

CAS机制

那么我们再看看看CAS的底层是怎么实现的?

以AtomicInteger为例,上源码:

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}
private volatile int value;
public final int get() {
    return value;
}

这段代码是一个无限循环,也就是CAS的自旋。循环体做了三件事:

  1. 获取当前值
  2. 当前值+1,计算出目标值
  3. 进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤。

    get方法,获取的变量的当前值。如何保证当前值是内存中的最新值那??上面我们看到了volatile关键字。

再来看一下compareAndSet方法:

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

unsafe是JVM提供的一个硬件级别的原子操作
unsafe的compareAndSwapInt方法参数包括了这三个基本元素:valueOffset参数代表了V,expect参数代表了A,update参数代表了B。

总结
有不对的地方,欢迎留言!

上一篇: Linux Mutex机制分析

下一篇: CAS机制