Java并发编程:什么是CAS?这回总算知道了

无锁的思想

众所周知,Java中对并发控制的最多见方法就是锁,锁能保证同一时刻只能有一个线程访问临界区的资源,从而实现线程安全。然而,锁虽然有效,但采用的是一种悲观的策略。它假设每一次对临界区资源的访问都会发生冲突,当有一个线程访问资源,其余线程就必须等待,因此锁是会阻塞线程执行的。java

固然,凡事都有两面,有悲观就会有乐观。而无锁就是一种乐观的策略,它假设线程对资源的访问是没有冲突的,同时全部的线程执行都不须要等待,能够持续执行。若是遇到冲突的话,就使用一种叫作CAS (比较交换) 的技术来鉴别线程冲突,若是检测到冲突发生,就重试当前操做到没有冲突为止。算法

CAS概述

CAS的全称是 Compare-and-Swap,也就是比较并交换,是并发编程中一种经常使用的算法。它包含了三个参数:V,A,B。编程

其中,V表示要读写的内存位置,A表示旧的预期值,B表示新值安全

CAS指令执行时,当且仅当V的值等于预期值A时,才会将V的值设为B,若是V和A不一样,说明多是其余线程作了更新,那么当前线程就什么都不作,最后,CAS返回的是V的真实值。多线程

而在多线程的状况下,当多个线程同时使用CAS操做一个变量时,只有一个会成功并更新值,其他线程均会失败,但失败的线程不会被挂起,而是不断的再次循环重试。正是基于这样的原理,CAS即时没有使用锁,也能发现其余线程对当前线程的干扰,从而进行及时的处理。并发

CAS的应用类

Java中提供了一系列应用CAS操做的类,这些类位于java.util.concurrent.atomic包下,其中最经常使用的就是AtomicInteger,该类能够看作是实现了CAS操做的Integer,因此,下面咱们就经过学习该类的案例来一窥全貌CAS的妙用。编辑器

学习AtomicInteger以前,咱们先来看一段代码实例:ide

public class AtomicDemo {

    public static int NUMBER = 0;

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

    public static void main(String[] args) throws InterruptedException {
        AtomicDemo test = new AtomicDemo();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++)
                    test.increase();
            }).start();
        }
        Thread.sleep(200);
        System.out.println(test.NUMBER);
    }
}

在main函数中开启了10个线程,执行后会轮流调用 increase(),固然咱们知道,运行后输出的结果确定不是咱们指望的值,由于没有作线程安全的处理,因此10个线程流量操做临界区的资源NUMBER就会出错。函数

解决办法并不难,用咱们以前学过的锁,例如synchronized修饰代码块,程序就会正常输出10000。固然,用锁解决并非咱们想要的方式,由于锁会阻塞线程,影响程序的性能,这时候,AtomicInteger就能够派上用场了。性能

将上面的程序改造一下,变成下面这样:

public static AtomicInteger NUMBER = new AtomicInteger(0);

public static void increase() {
    NUMBER.getAndIncrement();
}

public static void main(String[] args) throws InterruptedException {
    AtomicDemo test = new AtomicDemo();
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            for (int j = 0; j < 1000; j++)
                test.increase();
        }).start();
    }
    Thread.sleep(200);
    System.out.println(test.NUMBER);
}

运行main方法,程序输出的就是咱们想要的值,也就是10000。

上面的代码中,increase方法里调用了NUMBER.getAndIncrement() ,这是AtomicInteger的自增方法,会对当前的值加1,而且返回旧值,点进方法的源码,它调用的是unsafe.getAndAddInt()方法:

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

getAndAddInt的做用是对当前值加1,并返回旧值。

unsafe是Unsafe类的一个变量,经过Unsafe.getUnsafe()来获取

private static final Unsafe unsafe = Unsafe.getUnsafe();

Unsafe类是一个比较特殊的类,它是一个JDK内部使用的专属类,用通常的编辑器没法直接查看源码,只能看到反编译后的class文件。

这里要扩展一个知识点,就是Java自己没法访问操做系统,须要使用native方法,而Unsafe类中的方法就包含了大量的native方法,提升了Java对系统底层的原子操做能力。例如咱们代码中使用到的getAndAddInt()底层就是调用一个native方法,用idea点击方法,获得下面反编译后的代码:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

compareAndSwapInt的做用是比较并交换整数值,若是指定的字段的值等于指望值,也就是CAS中的 'A' (预期值),那么就会把它设置为新值 (CAS中的 'B'),不难想象,该方法内部的实现必然是依靠原子操做完成的。除此以外,Unsafe类中还提供了其余的原子操做的方法,例如上面源码中的getIntVolatile就是使用volatile语义得到给定对象的值,这些方法经过底层的原子操做高效的提高了应用层面的性能。

CAS的缺点

虽然CAS的性能比起锁要强大不少,但它也存在一些缺点,例如:

一、循环的时间开销大

在getAndAddInt的方法中,咱们能够看到,只是简单的设置一个值却调用了循环,若是CAS失败,会一直进行尝试。若是CAS长时间不成功,那么循环就会不停的跑,无疑会给系统形成很大的开销。

二、ABA问题

前面说过,CAS判断变量操做成功的条件是V的值和A是一致的,这个逻辑有个小小的缺陷,就是若是V的值一开始为A,在准备修改成新值前的期间曾经被改为了B,后来又被改回为A,通过两次的线程修改对象的值仍是旧值,那么CAS操做就会误任务该变量历来没被修改过。这就是CAS中的“ABA”问题。

固然,"ABA"问题也有解决方案,Java并发包中提供了一个带有时间戳的对象引用 AtomicStampedReference,其内部不只维护了一个对象值,还维护了一个时间戳,当AtomicStampedReference对应的数值被修改时,除了更新数据自己,还须要更新时间戳,只有对象值和时间戳都知足指望值,才能修改为功。这是AtomicStampedReference的几个有关时间戳信息的方法:

//比较设置 参数依次为:指望值 写入新值 指望时间戳 新时间戳
public boolean compareAndSet(V expectedReference, V newReference,
                             int expectedStamp, int newStamp)
//得到当前时间戳
public int getStamp()
//设置当前对象引用和时间戳
public void set(V newReference, int newStamp)
相关文章
相关标签/搜索