java 1.5引进原子类,在java.util.concurrent.atomic包下一共提供了13个类,分为4种类型,分别是:

  • 原子更新基本类型:AtomicLong、AtomicInteger、AtomicBoolean
  • 原子更新数组:AtomicLongArray、AtomicIntegerArray、AtomicReferenceArray
  • 原子更新引用:AtomicReference、AtomicMarkableReference、AtomicStampedReference
  • 原子更新属性:AtomicLongFieldUpdater、AtomicIntegerFieldUpdater、AtomicReferenceUpdater

原子类也是java实现同步的一套解决方案。既然已经有了synchronized关键字和lock,为什么还要引入原子类呢?或者什么场景下使用原子类更好呢?

在很多时候,我们需要的仅仅是一个简单的、高效的、线程安全的递增或者递减方案,这个方案一般需要满足以下要求:

  1. 简单:操作简单,底层实现简单
  2. 高效:占用资源少,操作速度快
  3. 安全:在高并发和多线程环境下要保证数据的正确性

对于是需要简单的递增或者递减的需求场景,使用synchronized关键字和lock固然可以实现,但代码写的会略显冗余,且性能会有影响,此时用原子类更加方便。

上面的原子类中,提供了一些无锁的方法,以AtomicInteger类为例:

  • int get() :直接返回值
  • void set(int) :设置数据(注意这里是没有原子性操作的)
  • int getAndIncrement():以原子方式将当前值加1,返回操作之前的值,相当于线程安全的i++操作
  • int incrementAndGet():以原子方式将当前值加1,返回操作之后的值,相当于线程安全的++i操作
  • int getAndDecrement():以原子方式将当前值减1,返回操作之前的值,相当于线程安全的i--操作
  • int decrementAndGet():以原子方式将当前值减1,返回操作之后的值,相当于线程安全的--i操作
  • int addAndGet(int):增加指定的数据,返回增加后的数据
  • int getAndAdd(int):增加指定的数据,返回变化前的数据
  • int getAndSet(int):设置指定的数据,返回设置前的数据
  • void lazySet(int):仅仅当get时才会set

除此之外,还提供了compareAndSet、weakCompareAndSet这两个方法。对于初学者来说,很多人都不清楚二者的区别,本文就重点来解释一下。

基于jdk8的实现

boolean compareAndSet(int expect ,int newValue)、boolean weakCompareAndSet(int expect ,int newValue) 这两个方法都是使用原子的方式更新值,如果当前值和expect相等,将其更新成newValue并且返回true,否则返回false;二者的区别是在后者的api文档中加了一句: May fail spuriously and does not provide ordering guarantees, so is only rarely an appropriate alternative to compareAndSet.这里最重要的是“fail spuriously”和“not provide ordering guarantees”。为了理解这一点,在jdk8的​​atomic​​文档上找到:

  • weakCompareAndSet方法有适用性的限制,在一些平台上,正常情况下weak版本比compareAndSet更高效,但weakCompareAndSet方法的调用都可能会返回一个虚假的失败( 无任何明显的原因 )。一个失败的返回意味着,操作将会重新执行如果需要的话,重复操作依赖的保证是当变量持有expectedValue的值并且没有其他的线程也尝试设置这个值将最终操作成功。( 一个虚假的失败可能是由于内存冲突的影响,而和预期值(expectedValue)和当前的值是否相等无关 )。此外weakCompareAndSet并不会提供排序的保证,即通常需要用于同步控制的排序保证。
  • weakCompareAndSet实现了一个变量原子的读操作和有条件的原子写操作,但是它不会创建任何happen-before排序,所以该方法不提供对weakCompareAndSet操作的目标变量以外的变量的在之前或在之后的读或写操作的有序保证。

1、volatile关键字

1)volatile变量​​自身​​具有下列特性:

  1. 可见性/一致性:对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入。
  2. 原子性:对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++这种复合操作不具有原子性。

volatile是如何保证可见性的了?
在多核处理器中,当进行一个volatile变量的写操作时,JIT编译器生成的汇编指令会在写操作的指令前加上一个“lock”前缀。“lock”前缀的指令在多核处理器下会引发了两件事情:

  1. 将当前处理器缓存行的数据会写回到系统内存。
  2. 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。

因此更确切的来说,因为操作缓存的最小单位为一个缓存行,所以每次对volatile变量自身的操作,都会使其所在缓存行的数据会写回到主存中,这就使得其他任意线程对该缓存行中变量的读操作总是能看到最新写入的值( 会从主存中重新载入该缓存行到线程的本地缓存中 )。当然,也正是因为缓存每次更新的最小单位为一个缓存行,这导致在某些情况下程序可能出现“伪共享”的问题。嗯,好像有些个跑题,“伪共享”并不属于本文范畴,这里就不进行展开讨论。

好了,目前为止我们已经了解volatile变量自身所具有的特性了。注意,这里只是volatile自身所具有的特性,而volatile对​​线程的内存​​可见性的影响比volatile自身的特性更为重要。

2)volatile 写-读建立的 happens before 关系
happens-before 规则中有这么一条:volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

happens-before的这个规则会保证volatile写-读具有如下的内存语义:

  • volatile写的内存语义:
    当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的所有共享变量值刷新到主内存。
  • volatile读的内存语义:
    当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取所有共享变量。

为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。因为内存屏障是一组处理器指令,它并不由JVM直接暴露,因此JVM会根据不同的操作系统插入不同的指令以达成我们所要内存屏障效果。
从整体执行效率的角度考虑,JMM 选择了在每个 volatile 写的后面插入一个 StoreLoad 屏障。

StoreLoad屏障
指令示例:Store1; StoreLoad; Load2
确保Store1数据对其他处理器变得可见(指刷新到内存)先于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果(LoadLoad Barriers、StoreStore Barriers、LoadStore Barriers)

好了,到现在我们知道了volatile的内存语义( happens-before关系 )会保证volatile写操作之前的读写操作不会被重排序到volatile写操作之后,并且保证了写操作后将线程本地内存(可能包含了多个缓存行)中所有的共享变量值都刷新到主内存中。这样其他线程总是能在volatile写操作后的读取操作中得到该线程中所有共享变量的正确值。这是volatile的happens-before关系( 通过内存屏障实现 )带给我们的结果。注意,这个和volatile变量自身的特性是不同的,volatile自身仅仅是保证了volatile变量本身的可见性。而volatile的happens-before关系则保证了操作不会被重排序的同时保证了线程本地内存中所有共享变量的可见性。

好了,讨论到这里,我们重新来理解下weakCompareAndSet的实现语义。也就是说,weakCompareAndSet操作仅保留了volatile自身变量的特性,而出去了happens-before规则带来的内存语义。也就是说,weakCompareAndSet无法保证处理操作目标的volatile变量外的其他变量的执行顺序( 编译器和处理器为了优化程序性能而对指令序列进行重新排序 ),同时也无法保证这些变量的可见性。
 

2、源码的角度来分析

public boolean compareAndSet(T obj, int expect, int update) {
if (obj == null || obj.getClass() != tclass || cclass != null) fullCheck(obj);
return unsafe.compareAndSwapInt(obj, offset, expect, update);
}

public boolean weakCompareAndSet(T obj, int expect, int update) {
if (obj == null || obj.getClass() != tclass || cclass != null) fullCheck(obj);
return unsafe.compareAndSwapInt(obj, offset, expect, update);
}

是的,你没有看错。在jdk1.8中这两个方法的实现完全一样。。『unsafe.compareAndSwapInt(obj, offset, expect, update);』中就是调用native方法了:

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}

这是因为:
在一些平台上存在硬件的CAS单指令(即,CAS使用一条机器指令就能完成),那么atomic_compare_exchange_weak和atomic_compare_exchange_strong本质上就是一样的了,因为它们都归结为单指令cmpxchg(如x86上。👆的例子就是x86系统的);但在不存在单一硬件的CAS指令的平台上,atomic_compare_exchange_strong和atomic_compare_exchange_weak都是使用LL/SC(like ARM, PowerPC, etc)两条汇编指令实现的。👈也就说它是多指令完成CAS的。那么就可能出现在LL 与 SC 两条指令在执行的间期发了上下文切换,或者其他加载和存储操作,这都将导致一个store-conditional的spuriously fail。

基于JDK 9

在JDK 9中 compareAndSet 和 weakCompareAndSet方法的实现有些许的不同

/**
* Atomically updates Java variable to {@code x} if it is currently
* holding {@code expected}.
*
* <p>This operation has memory semantics of a {@code volatile} read
* and write. Corresponds to C11 atomic_compare_exchange_strong.
*
* @return {@code true} if successful
*/
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);

底层调用的native方法的实现中,cmpxchgb指令前都会有“lock”前缀了(在JDK 8中,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。只有在CPU是多处理器(multi processors)的时候,会添加一个lock前缀)。

同时多了一个@HotSpotIntrinsicCandidate注解,该注解是特定于Java虚拟机的注解。通过该注解表示的方法可能( 但不保证 )通过HotSpot VM自己来写汇编或IR编译器来实现该方法以提供性能。它表示注释的方法可能(但不能保证)由HotSpot虚拟机内在化。如果HotSpot VM用手写汇编和/或手写编译器IR(编译器本身)替换注释的方法以提高性能,则方法是内在的。
也就是说虽然外面看到的在JDK9中weakCompareAndSet和compareAndSet底层依旧是调用了一样的代码,但是不排除HotSpot VM会手动来实现weakCompareAndSet真正含义的功能的可能性。

补充:JDK 1.9提供了Variable Handles的API,主要是用来取代java.util.concurrent.atomic包以及sun.misc.Unsafe类的功能。Variable Handles需要依赖jvm的增强及编译器的协助,即需要依赖java语言规范及jvm规范的升级。

参考:

​https://www.jianshu.com/p/55a66113bc54​