《Java多线程之CAS》

《Java多线程之CAS》

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

1、互斥同步

2、非阻塞同步

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

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

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

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

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

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

1、Test-and-Set

2、Fetch-and-add

3、Swap

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

5、Load-Link/Store-Conditional

下面主要介绍下CAS。

CAS

乐观锁用到的机制就是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);
    }

CAS的”ABA”问题

尽管CAS看起来很完美,但存在这样一个逻辑的漏洞:如果一个V初次读取的是A值,并且在准备赋值的时候检查到它还是A值,那我们就说明它没有被其它线程改变过吗??这显然是不对的。如果是两个线程同时工作,可能会发生这样的场景:如果在这段期间它的值被改为了B,又被改为了A,那CAS操作就认为它没有改变过。

问题基本是这个样子:

  • 1、进程P1在共享变量中读到值为A
  • 2、P1被抢占了,进程P2执行
  • 3、P2把共享变量里的值从A改成了B,再改回到A,此时被P1抢占。
  • 4、P1回来看到共享变量里的值没有被改变,于是继续执行。

虽然P1以为变量值没有改变,继续执行了,但是这个会引发一些潜在的问题。ABA问题最容易发生在lock free 的算法中的,CAS首当其冲,因为CAS判断的是指针的地址。如果这个地址被重用了呢,问题就很大了。(地址被重用是很经常发生的,一个内存分配后释放了,再分配,很有可能还是原来的地址)

这个例子你可能没有看懂,维基百科上给了一个活生生的例子——

  • 你拿着一个装满钱的手提箱在飞机场,此时过来了一个火辣性感的美女,然后她很暖昧地挑逗着你,并趁你不注意的时候,把用一个一模一样的手提箱和你那装满钱的箱子调了个包,然后就离开了,你看到你的手提箱还在那,于是就提着手提箱去赶飞机去了。

这就是ABA的问题。

关于如何解决ABA问题:使用版本号。具体如下:

ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

CAS不仅只存在”ABA”问题,还存在如下两个缺点:

1、循环时间长开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

2、只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

参考资料

1、《深入理解Java虚拟机》

2、http://coolshell.cn/articles/8239.html

你可能感兴趣的:(深入理解java虚拟,java,多线程,线程安全,并发,CAS)