Java并发——原子变量类

2,343 阅读11分钟

原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意 为“不可被中断的一个或一系列操作”。

非阻塞同步    

        用底层的原子机器指令(例如比较并交换指令)代替锁来确保数据在并发访问中的一致性。这种乐观并发策略的实现不再需要把线程阻塞挂起,因此这种同步操作被称为非阻塞同步(Non-Blocking Synchronization)。

非阻塞算法

        如果在某种算法中,一个线程的失败或挂起不会导致其他线程也失败或挂起,那么这种算法就被称为非阻塞算法。如果在算法的每个步骤中都存在某个线程能够执行下去,那么这种算法也被称为无锁(Lock-Free)算法。在JVM从一个版本升级到下一个版本的过程中,并发性能的主要提升都来自于(在JVM内部以及平台类库中)对非阻塞算法的使用。

        近年来,在并发算法领域的大多数研究都侧重于非阻塞算法,这种算法用底层的原子机器指令(例如比较并交换指令)代替锁来确保数据在并发访问中的一致性。非阻塞算法被广泛地用于在操作系统和JVM中实现线程/进程调度机制、垃圾回收机制以及锁和其他并发数据结构。 与基于锁的方案相比,非阻塞算法在设计和实现上都要复杂得多,但它们在可伸缩性和活跃性上却拥有巨大的优势。由于非阻塞算法可以使多个线程在竞争相同的数据时不会发生阻塞,因此它能在粒度更细的层次上进行协调,并且极大地减少调度开销。而且,在非阻塞算法中不存在死锁和其他活跃性问题。Java中,可以使用原子变量类(例如AtomicInteger和AtomicReference)来构建高效的非阻塞算法。

硬件对并发的支持

        在针对多处理器操作而设计的处理器中提供了一些特殊指令,用于管理对共享数据的并发访问。

  • 测试并设置(Test-and-Set)

  • 获取并递增(Fetch-and-Increment)

  • 交换(Swap)

  • 比较并交换(Compare-and-Swap,简称CAS)

  • 关联加载/条件存储(Load-Linked/Store-Conditional)

前面的三条是20世纪就已经存在于大多数指令集之中的处理器指令,这些指令足以实现各种互斥体,而这些互斥体又可以实现一些更复杂的并发对象。两条是现代处理器新增的,而且这两条指令的目的和功能也是类似的。在IA64、x86指令集中有用cmp xchg指令完成的CAS功能,在SPARC-TSO中也有用casa指令实现的,而在ARM和PowerPC架构下,则需要使用一对 ldrex/strex指令来完成LL/SC的功能。操作系统和JVM使用这些指令来实现锁和并发的数据结构,但在Java5.0之前,在Java类中还不能直接使用这些指令。

比较并交换(CAS指令)

        CAS的典型使用模式是:CAS指令需要有三个操作数,分别是内存位置V(在Java中可以简单地理解为变量的内存地址,用V 表示)、旧的预期值A和准备设置的新值B。CAS指令执行时,当且仅当V符合A时,处理器才会用B更新V的值,否则它就不执行更新。但是,不管是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作,执行期间不会被其他线程中断。由于CAS能检测到来自其他线程的干扰,因此即使不使用锁也能够实现原子的读一改一写操作序列。

      当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都将失败。然而,失败的线程并不会被挂起,而是被告知在这次竞争中失败,并可以再次尝试。由于一个线程在竞争CAS时失败不会阻塞,因此它可以决定是否重新尝试,或者执行一些恢复操作,也或者不执行任何操作。这种灵活性就大大减少了与锁相关的活跃性风险。 

Java使用CAS操作    

        在支持CAS的平台上,运行时JVM把它们编译为相应的(多条)机器指令。在最坏的情况下,如果不支持CAS指令,那么JVM将使用自旋锁。在原子变量类(例如java.util.concurrent.atomic中的AtomicXxx)中使用了这些底层的JVM支持为数字类型和引用类型提供一种高效的CAS操作,而在java.util. concurrent中的大多数类在实现时则直接或间接地使用了这些原子变量类。

        Java类库中使用CAS操作,该操作由sun.misc .Unsafe类里面的 compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供。Java虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果就是一条平台相关的处理器CAS指令,没有方法调用的过程。不过由于Unsafe类在设计上就不是提供给用户程序调用的类 (Unsafe::getUnsafe()的代码中限制了只有启动类加载器(Bootstrap ClassLoader)加载的Class才能访问它),因此在JDK 9之前只有Java类库可以使用CAS,譬如J.U.C包里面的整数原子类,其中的 compareAndSet()和getAndIncrement()等方法都使用了Unsafe类的CAS操作来实现。而如果用户程序也有使用CAS操作的需求,那要么就采用反射手段突破Unsafe的访问限制,要么就只能通过Java类库API来间接使用它。直到JDK9之后,Java类库才在VarHandle类里开放了面向用户程序使用的CAS操作。

Unsafe提供了3种CAS方法:

  • compareAndSwapObject()
  • compareAndSwapInt()
  • compareAndSwapLong()

原子变量Atomic

原子基本类型类

Atomic包提供了以下3个类: 

  • AtomicBoolean:原子更新布尔类型。 
  • AtomicInteger:原子更新整型。 
  • AtomicLong:原子更新长整型。

以上3个类提供的方法几乎一模一样,以AtomicInteger为例, AtomicInteger的常用方法:

  • int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的 value)相加,并返回结果。
  • boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
  • int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。
  • int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。

以getAndIncrement为例,说明其原子操作过程

 //AtomicInteger类getAndIncrement方法
public final int getAndIncrement() {	return unsafe.getAndAddInt(this, valueOffset, 1);
 }

//unsafe类getAndAddInt方法
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: 操作的对象 var2: 读取的内存位置V 
var4:要增加的值 var5: 旧的预期值A    
var5 + var4: 新值B

      首先从V中读取值旧的预期值A,并根据A计算新值B,然后再通过CAS指令(compareAndSwapInt)以原子方式将V中的值由A变成B(只要在这期间没有任何线程将V的值修改为其他值)。如果CAS失败,那么该操作将立即重试直到修改成功为止。

Atomic包只提供了3种基本类型的原子更新,我们先看下AtomicBoolean源码,发现它是先把Boolean转换成整型,再使用compareAndSwapInt进行CAS,所以原子更新char、float和double变量也可以用类似思路与int类型进行转换。

原子数组类

通过原子的方式更新数组里的某个元素,Atomic包提供了以下3个类:

  • AtomicIntegerArray:原子更新整型数组里的元素。
  • AtomicLongArray:原子更新长整型数组里的元素。
  • AtomicReferenceArray:原子更新引用类型数组里的元素。

以AtomicIntegerArray为例, AtomicIntegerArray的常用方法:

  • int getAndAdd(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。 

  • boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。

    //构造方法 public AtomicIntegerArray(int length) { array = new int[length]; } public AtomicIntegerArray(int[] array) { this.array = array.clone(); }

数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。

//AtomicIntegerArray类getAndAdd方法
public final int getAndAdd(int i, int delta) { 
   return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
}
//数组下标检查
private long checkedByteOffset(int var1) {
	if (var1 >= 0 && var1 < this.array.length) {
		return byteOffset(var1);
	} else {
		throw new IndexOutOfBoundsException("index " + var1);
	}
}
//计算内存偏移量地址
private static long byteOffset(int var0) {
	return ((long)var0 << shift) + (long)base;
}

getAndAdd方法与上面AtomicInteger一直调用的还是unsafe.getAndAddInt()方法,在取数据之前会进行下标检查,然后计算内存偏移量地址。

原子更新引用类

原子更新引用类型,Atomic包提供了以下3个类:

  • AtomicReference:原子更新引用类型。 
  • AtomicStampedReference:原子更新带有版本号的引用类型。
  • AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,boolean initialMark)。

以AtomicReference为例, AtomicReference的常用方法:

  • V getAndSet(V newValue):将原子设置为给定值并返回旧值。

  • boolean compareAndSet(V expect, V update):如果当前值 ==为预期值,则将值设置为给定的更新值。

    public final boolean compareAndSet(V expect, V update) { return unsafe.compareAndSwapObject(this, valueOffset, expect, update); }

调用的Unsafe类compareAndSwapObject()方法, 原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。

原子更新字段类

如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic包提供了以下3个类进行原子字段更新:

  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。 
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器。
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段。

要想原子地更新字段类需要两步。第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新类的字段(属性)必须使用public volatile修饰符。

以AtomicIntegerFieldUpdater为例

// 创建原子更新器,并设置需要更新的对象类和对象的属性
private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.
newUpdater(User.class"old");

AstomicIntegerFieldUpdater没有与某个特定的实例关联在一起,因而可以更新目标类的任意实例中的字段。更新字段类提供的原子性保证比普通原子类更弱一些,因为无法保证底层的字段不被直接修改—compareAndSet以及其他方法只能确保其他使用此类方法的线程的原子性。几乎在所有情况下,普通原子变量的性能都很不错,只有在很少的情况下才需要使用原子更新字段类。(如果在执行原子更新的同时还需要维持现有类的串行化形式,那么原子更新字段将非常有用。)

JDK8新增LongAdder

JDK8在atomic包新增了5个类,分别是Striped64,LongAdder,LongAccumulator,DoubleAdder,DoubleAccumulator。其中,Sriped64作为父类。

《阿里巴巴Java开发手册》建议在JDK8下面使用LongAdder代替AtomicLong

LongAdder

       上面的AtomicLong通过CAS提供了非阻塞的原子性操作,在高并发下大量线程会同时去竞争更新同一个原子变量,但是由于同时只有一个线程的CAS操作会成功,这就造成了大量线程竞争失败后,会通过无限循环不断进行自旋尝试CAS的操作,而这会白白浪费CPU资源。

JDK8新增了一个原子性递增或者递减类LongAdder用来克服在高并发下使用AtomicLong的缺点。把一个变量分解为多个变量,让同样多的线程去竞争多个资源,LongAdder就是这个思路。

    使用LongAdder时,则是在内部维护多个Cell变量,每个Cell里面有一个初始值为0的long型变量,这样,在同等并发量的情况下,争夺单个变量更新操作的线程量会减少,这变相地减少了争夺共享资源的并发量。另外,多个线程在争夺同一个Cell原子变量时如果失败了,它并不是在当前Cell变量上一直自旋CAS重试,而是尝试在其他Cell的变量上进行CAS尝试,这个改变增加了当前线程重试CAS成功的可能性。 最后,在获取LongAdder当前值时,是把所有Cell变量的value值累加后再加上base返回的。 

//Cell类使用@sun.misc.Contended修饰是为了避免伪共享
@sun.misc.Contended static final class Cell {
        //Cell内部维护一个被声明为volatile的变量
	volatile long value;
	Cell(long x) { value = x; }
	final boolean cas(long cmp, long val) {
        //通过cas函数(CAS操作),保证了当前线程更新时被分配的Cell元素中value值的原子性
		return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
	}

	// Unsafe mechanics
	private static final sun.misc.Unsafe UNSAFE;
	private static final long valueOffset;
	static {
		try {
			UNSAFE = sun.misc.Unsafe.getUnsafe();
			Class<?> ak = Cell.class;
			valueOffset = UNSAFE.objectFieldOffset
				(ak.getDeclaredField("value"));
		} catch (Exception e) {
			throw new Error(e);
		}
	}
}

       Cell内部维护一个被声明为volatile的变量。通过cas函数(CAS操作),保证了当前线程更新时被分配的Cell元素中value值的原子性。@sun.misc.Contended注解对Cell类进行字节填充,这防止了数组中多个元素共享一个缓存行,在性能上是一个提升。

public long sum() {
	Cell[] as = cells; Cell a;
	long sum = base;
	if (as != null) {
		for (int i = 0; i < as.length; ++i) {
			if ((a = as[i]) != null)
				sum += a.value;
		}
	}
	return sum;
}

        long sum()方法返回当前的值,内部操作是累加所有Cell内部的value值后再累加base。 例如下面的代码,由于计算总和时没有对Cell数组进行加锁,所以在累加过程中可能有其他线程对Cell中的值进行了修改,也有可能对数组进行了扩容,所以sum返回的值并不是非常精确的,其返回值并不是一个调用sum方法时的原子快照值。

public void add(long x) {
	Cell[] as; long b, v; int m; Cell a;
	if ((as = cells) != null || !casBase(b = base, b + x)) {
		boolean uncontended = true;
		if (as == null || (m = as.length - 1) < 0 ||
			(a = as[getProbe() & m]) == null ||
			!(uncontended = a.cas(v = a.value, v + x)))
			longAccumulate(x, null, uncontended);
	}
}
final boolean casBase(long cmp, long val) {
	return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}

如果cells不为null  casBase(b = base, b + x)就是对base进行CAS操作,执行成功的话操作就结束了,这时候就类似AtomicLong的操作。

 如果cells为null或者线程执行代码的CAS操作失败了调用longAccumulate()方法该方法主要是用一个死循环对cell数组进行初始化与扩容操作。

小结

       LongAdder维护了一个延迟初始化的原子性更新数组(默认情况下Cell数组是null)和一个基值变量base。由于Cells占用的内存是相对比较大的,所以一开始并不创建它,而是在需要时创建,也就是惰性加载。 当一开始判断Cell数组是null并且并发线程较少时,所有的累加操作都是对base变量进行的。保持Cell数组的大小为2的N次方,在初始化时Cell数组中的Cell元素个数为2,数组里面的变量实体是Cell类型。Cell类型是AtomicLong的一个改进,用来减少缓存的争用,也就是解决伪共享问题。 对于大多数孤立的多个原子操作进行字节填充是浪费的,因为原子性操作都是无规律地分散在内存中的(也就是说多个原子性变量的内存地址是不连续的),多个原子变量被放入同一个缓存行的可能性很小。但是原子性数组元素的内存地址是连续的,所以数组内的多个元素能经常共享缓存行,因此这里使用@sun.misc.Contended注解对Cell类进行字节填充,这防止了数组中多个元素共享一个缓存行,在性能上是一个提升。

LongAdder类继承自Striped64类,在Striped64内部维护着三个变量。 LongAdder的真实值其实是base的值与Cell数组里面所有Cell元素中的value值的累加,base是个基础值,默认为0。cellsBusy用来实现自旋锁,状态值只有0和1,当创建Cell元素,扩容Cell数组或者初始化Cell数组时,使用CAS操作该变量来保证同时只有一个线程可以进行其中之一的操作。

LongAccumulator

LongAdder 类是LongAccumulator的一个特例,LongAccumulator比LongAdder的功能更强大。

LongAccumulator相比于LongAdder,可以为累加器提供非0的初始值,后者只能提供默认的0值。另外,前者还可以指定累加规则,比如不进行累加而进行相乘,只需要在构造LongAccumulator时传入自定义的双目运算器即可,后者则内置累加的规则。

原子变量性能与问题

ABA问题: 

因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。
可以通过AtomicStampedReference以及AtomicMarkableReference来解决ABA问题:

AtomicStampedReference原子的更新数据和数据的版本号,通过在引用上加上“版本号”,compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是 否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

AtomicMarkableReference更新一个“对象引用-布尔值”true和false标记,来标记是否已被修改。

大部分情况下ABA问题不会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更为高效。

性能       

         在高度竞争的情况下,锁的性能将超过原子变量的性能,但在更真实的竞争情况下,原子变量的性能将超过锁的性能。这个结论在其他领域同样成立:当交通拥堵时,交通信号灯能够实现更高的吞吐量,而在低拥堵时,环岛能实现更高的吞吐量。这是因为锁在发生竞争时会挂起线程,从而降低了CPU的使用率和共享内存总线上的同步通信量。原子变量基于CAS的算法,在遇到竞争时将立即重试,这通常是一种正确的方法,但在激烈竞争环境下却导致了更多的竞争。

锁与原子变量在不同竞争程度上的性能差异很好地说明了各自的优势和劣势。在中低程度的竞争下,原子变量能提供更高的可伸缩性,而在高强度的竞争下,锁能够更有效地避免竞争。(在单CPU的系统上,基于CAS的算法在性能上同样会超过基于锁的算法,因为CAS在单CPU的系统上通常能执行成功)

第三条曲线,它是一个使用ThreadLocal来保存状态。这种实现方法改变了类的行为,如果能够避免使用共享状态,那么开销将会更小。我们可以通过提高处理竞争的效率来提高可伸缩性,但只有完全消除竞争,才能实现真正的可伸缩性。

原子变量是一种“更好的volatile变量”,从而为整数和对象引用提供原子的更新操作。

参考

深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)
Java并发编程实战
Java并发编程艺术
Java并发编程之美
阿里巴巴Java开发手册
JDK8API