CAS机制的的解释和总结

文章目录

    • CAS是什么?有什么作用?
    • CAS的工作原理?
    • CAS的缺点?
    • 怎么解决ABA问题呢?
    • CAS在虚拟机中的应用?

CAS是什么?有什么作用?

比较并交换(Compare-and-Swap,简称称CAS),CAS操作用来避免阻塞同步

CAS的工作原理?

CAS指令需要有三个操作数,
分别是内存位置(在Java中可以简单地理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和准备设置的新值(用B表示)。
CAS指令执行时,当且仅当V符合A时,处理器才会用B更新V的值,否则它就不执行更新。 但是,不管是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作,执行期间不会被其他线程中断。

如何通过CAS操作避免阻塞同步?

测试的代码如代码清单1所示。这段代码里我们曾经通过20个线程自增10000次的操作来证明volatile变量不具备原子性,那么如何才能让它具备原子性呢?
之前我们的解决方案是把race++操作或increase()方法用同步块包裹起来,这毫无疑问是一个解决方案,如:代码清单2
但是如果改成AtomicInteger原子自增运算 如:代码清单3所示的写法,效率将会提高许多。

代码清单1 volatile的运算

/**
 * volatile变量自增运算测试
 *
 * @author zzm
 */
public class VolatileTest {

    public static volatile int race = 0;

    public static void increase() {
        race++;
    }

    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }

        // 等待所有累加线程都结束
        while (Thread.activeCount() > 2)
            Thread.yield();

        System.out.println(race);
    }
}

代码清单2 synchronized包裹increase()方法的volatile的运算

public class VolatileTest {

    public static volatile int race = 0;

    public synchronized static void increase() {
        race++;
    }

    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }

        // 等待所有累加线程都结束
        while (Thread.activeCount() > 2)
            Thread.yield();

        System.out.println(race);
    }
}

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

尽管JAVA 1.6为synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然比较低。所以面对这种情况,我们就可以使用java中的“原子操作类”

所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。如AtomicBoolean,AtomicUInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。

代码清单3 Atomic的原子自增运算

/**
 * Atomic变量自增运算测试
 *
 * @author zzm
 */
public class AtomicTest {

    public static AtomicInteger race = new AtomicInteger(0);

    public static void increase() {
        race.incrementAndGet();
    }

    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) throws Exception {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 1)
            Thread.yield();

        System.out.println(race);
    }
}

使用AtomicInteger代替int后,程序输出了正确的结果,而Atomic操作类的底层正是用到了“CAS机制”
来看一下incrementAndGet()方法的原子性:
这段代码是一个无限循环,也就是CAS的自旋,循环体中做了三件事:

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

如代码清单4所示。

代码清单4 incrementAndGet()方法的JDK源码

/**
 * Atomically increment by one the current value.
 * @return the updated value
 */
public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}


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

CAS的缺点?

1) CPU开销过大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。

2) 不能保证代码块的原子性

CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

3) ABA问题

CAS存在一个逻辑漏洞:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,那就能说明它的值没有被其他线程改变过了吗?
这是不能的,因为如果在这段期间它的值曾经被改成B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA问题”。

怎么解决ABA问题呢?

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

在Java中,AtomicStampedReference类就实现了用版本号作比较额CAS机制。

CAS在虚拟机中的应用?

对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,
可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
解决方案:对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性

参考:深入理解java虚拟机 —— 周志明
文章:什么是CAS机制?

你可能感兴趣的:(java)