Java极致并发:(四)CAS与Atomic类型

Apr 20, 2016   #并发  #Java 
一、CAS和Atomic类

Compare-and-Set操作是多数硬件架构都支持的CPU指令,sun.misc.Unsafe类提供了执行CAS操作的方法。

CAS(object, expected, x),若object值等于expected,则将object设为x,并返回true。否则返回false。该操作是原子性的。

与使用独占锁保证操作的原子性不同,CAS是一种较为乐观且使用范围较为受限的操作。就使用范围而言,CAS只能做到更新某个变量的值,而锁则可以保证任意操作序列的原子性。其次CAS操作并不显示地保证可见性,因此CAS操作的对象应该使用volatile修饰符。

当CAS操作失败时,只需要重复操作直到CAS成功即可。因此使用CAS操作并不会阻塞线程,减少了争夺锁而带来的一切负面影响(上下文切换、Cache flush)。因此,高性能的同步数据结构大都使用CAS操作实现,如JDK中的ConcurrentHashMap等。

JDK中还提供了一些原始类型和应用类型的Atomic类。这些Atomic类提供了一些常用的原子性操作,其内部使用CAS操作来实现,相对于使用锁而言性能相对较高。这里需要注意的是大多数原子类型的读和写是原子性的,但long和doule例外。也就是说,即使仅仅读和写long或double类型的共享变量,也需要显示的原子机制(如锁,或使用对应的Atomic类)来保证。

如AtomicInteger类,其实现较为简单。内部有一个volatile的int value字段存储实际值。其他操作如自增,实现如下:

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

var1是该AtomicInteger对象,var2是value字段的内部对象存储偏移值,var4为增加值。使用do-while循环来确保原子性的CAS操作执行成功。

Atomic类中还有一个较为有意思的操作,即lazySet(long newValue)。文档中的描述很简单,“Eventually sets to the given value."。我们知道Atomic其实是使用CAS操作包装了一个volatile类型,而对于volatile的写操作是很昂贵的(后文会有测试)。lazySet通过松弛volatile写的立即可见语义,会带来一些性能提升。(通过Unsafe类,即使对于volatile修饰的字段也可以进行普通读写,对没有volatile修饰的字段也可以进行volatile读写)

此外JDK还提供了一些AtomicFiledUpdater类,诸如AtomicIntegerFieldUpdater、AtomicLongFieldUpdater等。有时候我们只是偶尔需要对某些字段进行volatile读写或进行原子操作,此时AtomicFiledUpdater可以发挥作用。此外还可以将AtomicFiledUpdater作为类静态字段,从而减少大量Atomic类带来的内存开销(一个AtomicInteger对象大约需要16个字节,而一个原始int只需要4个字节)。

二、高度竞争环境的CAS

理想情况下,CAS操作会在常量时间完成。但是,失败的CAS操作也会争夺内存设备、总线等资源,会拖累成功的CAS操作。即,在竞争激烈的情况下,成功的CAS操作用时会变高。例如对于AtomicInteger类而言,其getAndIncrement操作,在高度竞争的环境下,其吞吐率会降低。

如何提供高度竞争环境下的性能呢?一种方法与网络中的拥塞控制思想差不多,当CAS操作失败后,每个线程采取策略,尽量避免下一次的竞争。

有兴趣的读者可以看一下论文“Lightweight Contention Management for  Ecient Compare-and-Swap Operations"。该文提出了一些避免竞争的方法,如常量时间退让,指数退让等等竞争避免策略。实验显示,通过这些简单的避让策略,可以将成功的CAS操作的吞吐量提升近5倍。

下一篇文章将详细剖析volatile修饰符,并比较volatile读写和非volatile读写的性能对比。