从volatile来看 atomic类包的compareAndSet和weakCompareAndSet的理解区分这两个方法

作者:tomas家的小拨浪鼓
链接:https://www.jianshu.com/p/55a66113bc54
來源:简书

简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

volatile

volatile 的特性

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

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

Q:volatile是如何保证可见性的了

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

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

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

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)

按转载者的理解,作者想表达的是 Store1是存储数据到内存的操作; StoreLoad 是’StoreLoad‘屏障;Load2 是读取内存的数据的操作.然后 StoreLoad是为了确保Store1存储数据到内存的操作,能够先于Load2及所有屏障后面装载指令的操作。StoreLoad 屏障会使该屏障之前的所有内存访问指令(存储和装载指令)完成后才能执行该屏障之后的内存访问指令。StoreLoad 屏障是一种“全能型”的屏障,它同时具有其他3个屏障的效果[LoadLoad 屏障、StoreStore 屏障、LoadStore 屏障]

好了,到现在我们知道了volatile的内存语义( happens-before关系 )会保证volatile写操作之前的读写操作不会被重排序到volatile写操作之后,并且保证了写操作后将线程本地内存(可能包含了多个缓存行)中所有的共享变量值都刷新到主内存中。保证了其他线程总是在volatile写入后,能够读取中到该写入线程所共享出来的变量值。这是volatilehappens-before关系( 通过内存屏障实现 )给我们带来的结果。注意,这个是volatile变量自身所描述的特性是不同的,因为volatile他只是保证了volatile变量的可见性,而volatile的内存语义的happens-before关系还能让线程能够在刷新(写)操作后不会因为环境的资源重排序功能,而带来不能读取到最新最正确的值。并带来变量可见性。

转载这篇文章的作者觉得happens-before 的特性比volatile的可见性更重要.

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

源码实现

目前为止,我们已经能够明白compareAndSet方法和weakCompareAndSet方法的不同之处了。那么,接下来我们来看看这两个方法的具体实现:

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

是的,你没有看错。这两个方法的实现完全一样。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;
}

由此可见,在JDK8乃至之前的版本,weakCompareAndSet方法并没有被真是意义上的实现,目前该方法所呈现出来的效果与compareAndSet方法是一样的。

基于JDK 9
在JDK 9中 compareAndSetweakCompareAndSet方法的实现有些许的不同

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

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

猜你喜欢

转载自blog.csdn.net/lzcaqde/article/details/80868854