Java CAS机制详解

CAS目的:
html

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

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

上面说了这么多,如今咱们追求的是一种效率高,还要保证数据的安全的一种方法。
编程

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

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

什么是CAS:
并发

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

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

下面用代码简单的实现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是一个很好的选择。

相关文章
相关标签/搜索