CAS

并发技术

Posted by Cc1over on November 14, 2018

CAS

我们都知道线程安全的实现有两种方法:

  • 互斥同步

  • 非阻塞同步

一般,互斥同步在编程上采用synchronized关键字来进行同步。但是由于互斥同步在多线程并发的情况下存在线程阻塞、唤醒以及用户态和内核态之间的切换所引起的性能问题。

从处理方式上来说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(例如:加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,我们都需要去加锁、用户态内核态之间的切换以及检测是否有线程需要唤醒等操作。

随着硬件指令的发展,我们有了另外一种选择,基于冲突检测的乐观并发策略。

通俗的讲,就是先进行操作,如果没有其它的线程争用共享数据,那操作就成功了,如果共享数据有争用,产生了冲突,则采用补偿措施(最常见的补偿措施就是不断的重试,直到正确为止)。这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。

为什么要随着硬件指令的发展,我们才会有这种基于冲突检测的乐观并发策略呢??

这是因为我们需要保证操作和冲突检测的原子性。硬件保证从语义上需要多次操作的行为只需要一条处理器指定就能够完成。这样的指令有:

  • Test-and-Set

  • Fetch-and-add

  • Swap

  • Compare-and-Swap(比较和交换,简称CAS)

  • Load-Link/Store-Conditional 乐观锁用到的机制就是CAS,Compare and Swap。

CAS有3个操作数,分别为内存值V、旧的期望值A和新值B。当且仅当期望值A和内存值V相同时,处理器用新值B更新V的值,否则什么都不做。

也可以这么理解, CAS 的含义是“我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我原来的值是多少”

在JDK1.5之后,Java程序才可以使用CAS操作,该操作由sun.misc.Unsafe类里面的compareAndSwapInt()、compareAndSwapLong()等几个方法包装提供。虚拟机在内部对这些方法进行了特殊的处理,即时编译出来的结果是与平台有关的处理器CAS指令,不存在方法调用的过程,或者可以认为是无条件内联进去的。

例如,在java.util.Concurrent包里面的整数原子类,其中的compareAndSet()、getAndIncrement()、getAndDecrement()等操作都是使用了sun.misc.Unsafe类的CAS操作。

在AtomicInteger类中的compareAndSet方法的代码如下:

 /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

ABA问题

尽快CAS看起来很完美,但从语义上来说并不是完美的,存在这样一个逻辑漏洞

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它依然是A值,那么我们就认定它没有改变过。如果在这期间它的值被改为B,后来又改为A,那么CAS就会误认为它从来没有被改变过,这个漏洞也被成为“ABA”问题。

在C的内存管理机制中广泛使用的内存重用机制,如果是CAS更新的是指针,机会出现一些指针错乱的问题。常见的ABA问题解决方式,就是在更新的时候增加一个版本号,每次更新之后版本号+1,从而保证数据一致。

不过大部分情况下ABA问题都不会影响到程序的正确性,如果需要解决,可以特殊考虑下,或者采用传统的互斥同步会更好