Java CAS机制详解

CAS目的:

在多线程中为了保持数据的准确性,避免多个线程同时操作某个变量,很多情况下利用关键字synchronized实现同步锁,使用synchronized关键字修可以使操作的线程排队等待运行,可以说是一种悲观策略,认为线程会修改数据,所以开始就把持有锁的线程锁住,其他线程只能是挂起状态,等待锁的释放,所以同步锁带来了问题:

主要的效率问题:在线程执行的时候,获得锁的线程在运行,其他被挂起的线程只能等待着持有锁的线程释放锁才有机会运行(现在JVM可能根据持有锁的时间来操作线程是否是被挂起还是自旋等待),在效率上都浪费在等待上。很可能这种锁是没有必要的,其他线程没有修改数据。在很多的线程切换的时候,由于有同步锁,就要涉及到锁的释放,加锁,这又是一个很大的时间开销。这里涉及到操作系统上的知识,关于线程之间的切换(被挂起和恢复)中间还要经历中断,时间片等等。

上面说了这么多,现在我们追求的是一种效率高,还要保证数据的安全的一种方法。

与锁(阻塞机制)的方式相比有一种更有效地方法,非阻塞机制,同步锁带来了线程执行时候之间的阻塞,而这种非阻塞机制在多个线程竞争同一个数据的时候不会发生阻塞的情况,这样在时间上就可以节省出很多的时间。

想到这里,知道volatile的可能会想到用volatile,使用volatile不会造成阻塞,volatile保证了线程之间的内存可见性和程序执行的有序性可以说已经很好的解决了上面的问题,但是一个很重要的问题就是,volatile不能保证原子性,对于复合操作,例如i++这样的程序包含三个原子操作:取指,增加,赋值。在《Java并发编程实战》这本书上有这样的一句话:变量的新值依赖于旧值时就不能使用volatile变量。实际上就是说的类似于i++这样的操作。具体详见:volatile关键字解析

什么是CAS:

现在采取的是CAS(Compare And Swap比较和交换)解决了volatile不能保证原子性。CAS通常比锁定要快得多,但这取决于争用的程度。因为如果读取和比较之间的值发生变化,CAS可能会强制重试,所以不能说某个方法就是绝对的好。CAS的主要问题是编程比锁定更困难。还好jdk提供了一些类用于完成基本的操作。

CAS主要包含三个操作数,内存位置V,进行比较的原值A,和新值B。当位置V的值与A相等时,CAS才会通过原子方式用新值B来更新V,否则不会进行任何操作。无论位置V的值是否等于A,都将返回V原有的值。通俗点说:我认为V地址的值应该是A,如果是,V地址的值更新为B,否则不修改并告诉V的值实际为多少(无论如何这个值都会通知到V)。上面说到了同步锁 是一种悲观策略,CAS是一种乐观策略,每次都开放自己,不用担心其他线程会修改变量等数据,如果其他线程修改了数据,那么CAS会检测到并利用算法重新计算。CAS也是同时允许一个线程修改变量,其他的线程试图修改都将失败,但是相比于同步锁,CAS对于失败的线程不会将他们挂起,他们下次仍可以参加竞争,这也就是非阻塞机制的特点。

下面用代码简单的实现CAS原理:

/**
 * Created with IDEA
 *
 * @author DuzhenTong
 * @Date 2018/2/1
 * @Time 11:52
 */
public class SimpleCAS {

    private int value;

    public SimpleCAS(int value) {
        this.value = value;
    }

    public synchronized int get(){
        return value;
    }

    public synchronized int compareAndSwap(int expectedValue, int newValue){
        int oldValue = value;//获取旧值
        if(oldValue == expectedValue){//如果期望值与当前V位置的值相同就给予新值
            value = newValue;
        }
        return oldValue;//返回V位置原有的值
    }

    public synchronized boolean compareAndSet(int expectedValue, int newValue){
        return (expectedValue == compareAndSwap(expectedValue, newValue));
    }

    public static void main(String[] args) {
        SimpleCAS simpleCAS = new SimpleCAS(3);
        simpleCAS.compareAndSet(5, 10);
        System.out.println(simpleCAS.get());//3

        SimpleCAS simpleCAS1 = new SimpleCAS(1);
        simpleCAS1.compareAndSet(1, 6);
        System.out.println(simpleCAS1.get());//6
    }

}

从运行结果可以看出代码的原理:设置一个初始值(内存位置),期望值和新值进行比较,如果期望值和初始值一致,返回新值,否则返回初始值。意思是你在修改在一个变量A,假如它原来的值是3,所以你预期它是3,如果在你修改的时候,它被别的线程更新为5,那么就不符合你的预期,你的修改也不会生效

从Java5开始引入了底层的支持,在这之前需要开发人员编写相关的代码才可以实现CAS。在原子变量类Atomic***中(例如AtomicInteger、AtomicLong)可以看到CAS操作的代码,在这里的代码都是调用了底层(核心代码调用native修饰的方法)的实现方法。在AtomicInteger源码中可以看getAndSet方法和compareAndSet方法之间的关系,compareAndSet方法调用了底层的实现,该方法可以实现与一个volatile变量的读取和写入相同的效果。在前面说到了volatile不支持例如i++这样的复合操作,在Atomic***中提供了实现该操作的方法。JVM对CAS的支持通过这些原子类(Atomic***)暴露出来,供我们使用。

CAS带来的问题:

ABA问题:CAS在操作的时候会检查变量的值是否被更改过,如果没有则更新值,但是带来一个问题,最开始的值是A,接着变成B,最后又变成了A。经过检查这个值确实没有修改过,因为最后的值还是A,但是实际上这个值确实已经被修改过了。为了解决这个问题,在每次进行操作的时候加上一个版本号,每次操作的就是两个值,一个版本号和某个值,A——>B——>A问题就变成了1A——>2B——>3A。在jdk中提供了AtomicStampedReference类解决ABA问题,用Pair这个内部类实现,包含两个属性,分别代表版本号和引用,在compareAndSet中先对当前引用进行检查,再对版本号标志进行检查,只有全部相等才更新值。

时间问题:看起来CAS比锁的效率高,从阻塞机制变成了非阻塞机制,减少了线程之间等待的时间。每个方法不能绝对的比另一个好,在线程之间竞争程度大的时候,如果使用CAS,每次都有很多的线程在竞争,而锁可以避免这些状况,相反的情况,如果线程之间竞争程度小,使用CAS是一个很好的选择。

posted @ 2018-02-02 00:07  In_new  阅读(808)  评论(0编辑  收藏  举报