Java编发编程之原子操做与CAS原理分析

引子

以前的文章咱们简单介绍了线程安全的三个核心概念可见性和有序性和原子性, 那么这篇文章咱们就来分析一下原子性操做的实现原理java

原子操做

原子本意为不可分割的最小粒子,而原子操做则为不可中断的一个或者系列操做c++

注意的是对一部分操做保持了原子性并不意味着就不会发生线程安全问题, 而是要保证整个临界区都是原子性的。编程

下面咱们来分析一下cpu和java中如何实现原子操做缓存

Cpu实现

CPU使用基于缓存加锁或者总线加锁实现多个CPU的原子性操做安全

cpu自动保证基本内存操做的原子性 处理器保证从系统内存当中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其余处理器不能访问这个字节的内存地址,可是复杂的内存操做处理器不能自动保证其原子性,好比跨总线宽度,跨多个缓存行,跨页表的访问。 可是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操做的原子性多线程

  • 总线加锁使同时只有一个cpu能独占内存进行操做

缘由是有可能多个处理器同时从各自的缓存中读取变量,分别进行操做,而后分别写入系统内存当中。那么想要保证读改写共享变量的操做是原子的,就必须保证CPU1读改写共享变量的时候,CPU2不能操做缓存了该共享变量内存地址的缓存。并发

处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其余处理器的请求将被阻塞住,那么该处理器能够独占使用共享内存。优化

  • 缓存加锁 控制指定cpu缓存达到缓存一致性来防止同时修改相同的缓存数据

总线加锁存在的问题是在同一时刻咱们只需保证对某个内存地址的操做是原子性便可,但总线锁定把CPU和内存之间通讯锁住了,这使得锁按期间,其余处理器不能操做其余内存地址的数据,因此总线锁定的开销比较大,如今比较新的处理器在大多数时候使用缓存锁定代替总线锁定来进行优化,从而下降锁的粒度 频繁使用的内存会缓存在处理器的L1,L2和L3高速缓存里,那么原子操做就能够直接在处理器内部缓存中进行,并不须要声明总线锁。 所谓缓存加锁就是若是缓存在处理器缓存行中的内容在LOCK操做期间被锁定,当它执行锁操做回写内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并容许它的缓存一致性机制来保证操做的原子性,由于缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其余处理器回写已被锁定的缓存行的数据时会起缓存行无效。this

有两种状况下处理器不会使用缓存锁定。第一种状况是:当操做的数据不能被缓存在处理器内部,或操做的数据跨多个缓存行,则处理器会调用总线锁定。第二种状况是:有些处理器不支持缓存锁定,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定atom

cpu提供了LOCK前缀的指令来实现加锁机制。好比交换指令XADD,CMPXCHG和其余一些操做数和逻辑指令,好比ADD,OR等,被这些指令操做的内存区域就会加锁,致使其余处理器不能同时访问它

Java实现

在Java中有些操做中能够定义为原子性

  • 对引用变量和大部分基本类型变量(除long和double以外)的读写是原子的。
  • 对全部声明为volatile的变量(包括long和double变量)的读写操做是原子的

Java可以使用锁和CAS来实现原子操做,CAS既Compare and Swap比较并替换的意思。

CAS

咱们先来分析一段代码

public int a = 1;
public boolean compareAndSwapInt(int b) {
    if (a == 1) {
        a = b;
        return true;
    }
    return false;
}

试想这段代码在多线程并发下,会发生什么?咱们不妨来分析一下:

线程A执行到 a==1,正准备执行a = b时,线程B也正在运行a = b,并在线程A以前把a修改成2;最后线程A又把a修改为了3。结果就是两个线程同时修改了变量a,显然这种结果是没法符合预期的,没法肯定a的值。 解决方法也很简单,在compareAndSwapInt方法加锁同步,变成一个原子操做,同一时刻只有一个线程才能修改变量a。

CAS中的比较和替换是一组原子操做,不会被外部打断,先根据获取到内存当中当前的内存值,在将内存值和原值做比较,要是相等就修改成要修改的值,属于硬件级别的操做,效率比加锁操做高。

JDK中的atomic包的原子操做类都是基于CAS实现的,接下去咱们经过AtomicInteger来看看是如何经过CAS实现原子操做的

public class AtomicInteger extends Number implements java.io.Serializable {
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
    public final int get() {return value;}
}

Unsafe是CAS的核心类,JDK中有一个类Unsafe,它提供了硬件级别的原子操做。 valueOffset表示的是变量值在内存中的偏移地址,由于Unsafe就是根据内存偏移地址获取数据的原值的。 value是用volatile修饰的,保证了多线程之间看到的value值是同一份。 接下去,咱们看看AtomicInteger是如何实现并发下的累加操做:

public final int getAndAdd(int delta) {    
    return unsafe.getAndAddInt(this, valueOffset, delta);
}

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;
}

其中比较和替换操做放在unsafe类中实现。

若是如今线程A和线程B同时执行getAndAdd操做:

  • AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,线程A和线程B各自持有一份value的副本,值为3。
  • 线程A经过getIntVolatile方法获取到value值3,线程切换,线程A挂起。
  • 线程B经过getIntVolatile方法获取到value值3,并利用compareAndSwapInt方法比较内存值也为3,比较成功,修改内存值为2,线程切换,线程B挂起。
  • 线程A恢复,利用compareAndSwapInt方法比较,发现手里的值3和内存值2不一致,此时value正在被另一个线程修改,线程A不能修改value值。
  • 线程的compareAndSwapInt实现,循环判断线程A继续利用compareAndSwapInt进行比较并替换,直到compareAndSwapInt修改为功返回true。

咱们再看看Unsafe类中的compareAndSwapInt方法。

public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);

能够看到,这是一个本地方法调用,这个本地方法在调用c++代码,下面是对应于intel X86处理器的源代码片断。

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
    int mp = os::isMP(); //判断是不是多处理器
    _asm {
        mov edx, dest
        mov ecx, exchange_value
        mov eax, compare_value
        LOCK_IF_MP(mp)
        cmpxchg dword ptr [edx], ecx
    }
}

从上面的源码中能够看出,会根据当前处理器类型来决定是否为cmpxchg指令添加lock前缀。 若是是多处理器,为cmpxchg指令添加lock前缀,反之,就省略lock前缀。 而lock前缀的做用主要有下面几点

  • 使用CPU的原子操做确保对内存读改写操做的原子执行。
  • 禁止该指令,与前面和后面的读写指令重排序。
  • 把写缓冲区的全部数据刷新到内存中。

CAS存在的问题

CAS虽然很高效的解决原子操做,可是CAS仍然存在三大问题。

  • ABA问题

由于CAS须要在操做值的时候检查下值有没有发生变化,若是没有发生变化则更新,可是若是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,可是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,这样就能区分是否发送了变化

JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法做用是首先检查当前引用是否等于预期引用,而且当前标志是否等于预期标志,若是所有相等,则以原子方式将该引用和该标志的值设置为给定的更新值

  • 循环开销大,若是自旋不成功就会带来大量开销

由于同时修改相同的值的并发比较大就会致使,CAS很难成功,这样就致使会一直自旋,带来很大开销, 因此使用中要避免在并发太大的地方使用

  • 只能保证一个共享变量的原子操做

当对一个共享变量执行操做时,咱们可使用循环CAS的方式来保证原子操做,可是对多个共享变量操做时,循环CAS就没法保证操做的原子性,有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操做。JDK提供了AtomicReference类来保证引用对象之间的原子性,你能够把多个变量放在一个对象里来进行CAS操做。

固然Java中也能够经过加锁来实现原子操做,可是大多数状况锁的开销更大, 接下来的文章咱们会具体分析Java中锁的实现和原理

总结

本篇文章咱们讨论cpu和Java实现原子操做的方式,cpu实现原子操做有总线加锁和缓存加锁2种, 而Java主要经过CAS和锁来实现原子操做,经过分析源码咱们知道CAS其实是经过lock 指令调用cpu的加锁方式, 同时咱们讨论了CAS会带来的几个问题和解决方式,接下来的文章咱们将继续探讨Java并发编程的相关内容。

相关文章
相关标签/搜索