非阻塞式的原子性操作-CAS应用及原理

时间:2022-07-22
本文章向大家介绍非阻塞式的原子性操作-CAS应用及原理,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

一:问题抛出

假设在出现高并发的情况下对一个整数变量做依次递增操作,下面这两段代码是否会出现问题?

1.

public class IntegerTest  {
    private static Integer count = 0;
    synchronized public static void increment() {
        count++;
    }
}

2.

public class AtomicIntegerTest {
    private static AtomicInteger count = new AtomicInteger(0);
    public static void increment() {
        count.getAndIncrement();
    }
}

其实在使用Integer的时候,必须加上synchronized保证不会出现并发线程同时访问的情况,而在AtomicInteger中却不用加上synchronized,在这里AtomicInteger是提供原子操作的

二:先看下AtomicInteger类中属性和初始化的一些源码

unsafe:对应的是Unsafe类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。JDK中有一个类Unsafe,它提供了硬件级别的原子操作。JDK API文档也没有提供任何关于这个类的方法的解释。从描述可以了解到Unsafe提供了硬件级别的操作,比如说获取某个属性在内存中的位置,比如说修改对象的字段值,即使它是私有的。

value:volatile修饰的变量,内存中其他线程具有可见性。加或减都是对这个变量值进行修改。

valueOffset:这里指的就是value这个属性在内存中的偏移量(内存中的地址,而不是值),当类被加载时先按顺序初始化static变量和static块,通过unsafe中public native long objectFieldOffset(Field paramField);

/** Returns the memory address offset of the given static field.

   * The offset is merely used as a means to access a particular field
   * in the other methods of this class.  The value is unique to the given
   * field and the same value should be returned on each subsequent call.
   * 返回指定静态field的内存地址偏移量,在这个类的其他方法中这个值只是被用作一个访问
   * 特定field的一个方式。这个值对于 给定的field是唯一的,并且后续对该方法的调用都应该
   * 返回相同的值。
   *
   * @param field the field whose offset should be returned.
   *              需要返回偏移量的field
   * @return the offset of the given field.
   *         指定field的偏移量
   */
  public native long objectFieldOffset(Field field);

获取AtomicInteger类属性value在内存中的偏移量,并将偏移量值赋给valueOffset。需要强调valueOffset代表的不是value值在内存中的位置,而是这个属性在内存中的地址。

三:那么具体看下实现的源码

1.递增的方法:incrementAndGet()

getAndIncrement方法是在一个死循环里面调用compareAndSet方法,如果compareAndSet返回失败,就会一直从头开始循环,不会退出getAndIncrement方法,直到compareAndSet返回true。

 2.compareAndSet方法:

AtomicInteger中Unsafe实例调用compareAndSwapInt方法。

 3.compareAndSwapInt源码:

 看到这里知道是一个本地方法的调用,比较并置换,这里利用Unsafe类的JNI方法实现,使用CAS指令,可以保证读-改-写是一个原子操作。compareAndSwapInt有4个参数,this - 当前AtomicInteger对象,valueOffset- value属性在内存中的位置(需要强调的不是value值在内存中的位置),expect - 预期值,update - 新值,根据上面的CAS操作过程,当内存中的value值等于expect值时,则将内存中的value值更新为update值,并返回true,否则返回false。在这里我们有必要对Unsafe有一个简单点的认识,从名字上来看,不安全,确实,这个类是用于执行低级别的、不安全操作的方法集合,这个类中的方法大部分是对内存的直接操作,所以不安全,但当我们使用反射、并发包时,都间接的用到了Unsafe。

四:并发情况处理流程:

1.首先valueOffset获取value的偏移量,假设value=0,valueOffset=0(valueOffset其实是内存地址,便于表达-后面用valueOffset=n表示对应值的地址)。

2.线程A调用getAndIncrement方法,执行到161行,获取current=0,next=1,准备执行compareAndSet方法

3.线程B几乎与线程A同时调用getAndIncrement方法,执行完161行后,获取current=0,next=1,并且先于线程A执行compareAndSet方法,此时value=1,valueOffset=1

4.线程A调用compareAndSet发现预期值(current=0)与内存中对应的值(valueOffset=1,被线程B修改)不相等,即在本线程执行期间有被修改过,则放弃此次修改,返回false。

5.线程B接着循环,通过get()获取的值是最新的(volatile修饰的value的值会强迫线程从主内存获取),current=1,next=2,然后发现valueOffset=current=1,修改valueOffset=2。

五:总结下AtomicInteger的getAndIncrement方法之所以比普通Integer加减更适用并发环境:

1.current代表value最新的值是因为通过get()方法会从主内存读取(volatile,即读取valueOffset对应的值)

2.能够监测到get()读取到值到cpu执行compareAndSet执行成功之前被别的线程修改成功后的并发情况。

3.上面强调被别的线程“修改成功”是因为假如出现“ABA”情况是不会被觉察的。

即:如果一个变量初次读取的时候是A值,如果在这段期间它的值曾经被改成了B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个漏洞称为CAS操作的"ABA"问题。java.util.concurrent包为了解决这个问题,提供了一个带有标记的原子引用类"AtomicStampedReference",它可以通过控制变量值的版本来保证CAS的正确性。大部分情况下ABA问题并不会影响程序并发的正确性,如果需要解决ABA问题,使用传统的互斥同步可能回避原子类更加高效。