文章目录

  • 一、前言
  • 二、CPU的乱序执行
  • 三、乱序可能会造成的问题
  • 四、如何禁止指令重排序?
  • 4.1 Java 代码层面
  • 4.2 字节码层面
  • 4.3 HotSpot 源码层面
  • 4.4 CPU层面
  • 五、 hanppens-before原则 与 hanppens-before原则


一、前言

volatile,这个关键字,是JDK提供给我们使用的,之前也总结过一篇文章不过有点浅显,只是说明一下它的特性,这篇文章打算详细地总结下相关的概念与底层细节。

之前的总结:JUC(8)Java内存模型-JMM和Volatile关键字

volatile的特性:

  • 1、保证可见性 (这就要涉及JMM)
  • 2、不保证原子性
  • 3、禁止指令重排

二、CPU的乱序执行

CPU在执行代码时是乱序的,这里说的乱序是指代码编译后的指令的执行顺序,也就是说指令是乱序执行的。

为什么要打乱顺序呢?答:是为了提高CPU的执行效率。

这一块,具体的介绍可以看之前的读书笔记:为什么要进行指令重排呢?

在此,也可以给出一个代码示例来验证下:

执行下面代码,如果CPU没有乱序执行的话,那么 a = 1 必然在 x = b 前面,b = 1 必然在 y = a 的前面

我们可以得到什么结论,也就是 xy 肯定不能同时为 0

但是实际情况是,我们会遇到 xy 同时为 0的情况。(可能要循环几百万次才能遇到一次)

x = 0;
y = 0;
a = 0;
b = 0;
Thread one = new Thread(new Runnable() {
    public void run() {
        //由于线程one先启动,下面这句话让它等一等线程two. 可根据自己电脑的实际性能适当调整等待时间.
        //sleep(100000);
        a = 1;
        x = b;
    }
});

Thread two = new Thread(new Runnable() {
    public void run() {
        b = 1;
        y = a;
    }
});
one.start();
two.start();
one.join();
two.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if (x == 0 && y == 0) {
    System.err.println(result);
    break;
}

三、乱序可能会造成的问题

先说答案,会引起数据错误(数据安全问题)。

用个例子来验证吧,背景:DCL单例模式加了volatile关键字。

class T{
	int m = 8;
}
T t = new T();

反编译汇编码:

0 new #2 <T>
3 dup
4 invokespeecial # 3 <T.<init>>
7 astore_1
8 return 
12345

对汇编码逐步分析:

  • new #2 <T>:创建 m = 0 的对象并且栈帧中有一个引用指向该对象(此时m=0)
  • dup:在我们的栈帧中复制一份引用(此时m=0)
  • invokespecial #3 <T.<init>>:弹出一个栈帧中的值,实例化它的构造方法(此时m=8)
  • astore_1:将我们栈帧的引用赋值给 t,这里 1 指的是我们本地变量表中的第一位(此时m=8)

因为乱序的存在,当我们的 astore_1 在我们的 invokespeecial # 3 <T.<init>> 执行前执行,会导致我们的将我们没有实例化的对象赋值给 t,所以m = 0 。此时如果去使用t,肯定会造成数据错误。

所以为了避免这种现象,我们要对 DCLvolatile,那问题来了,我们 volatile 是怎么保证有序性的呢?

四、如何禁止指令重排序?

对于禁止指令重排序,从以下四个方面来谈:

  • 代码层面
  • 字节码层面
  • JVM层面
  • CPU层面

4.1 Java 代码层面

直接加一个 volatile 关键字即可

public class TestVolatile {
    public static volatile int counter = 1;

    public static void main(String[] args) {
        counter = 2;
        System.out.println(counter);
    }
}

4.2 字节码层面

在字节码层面,当对 volatile 进行反编译后,我们可以看到 VCC_volatile

我们对上述代码进行反编译,得到其 字节码

通过javac TestVolatile.java将类编译为class文件,再通过 javap -v TestVolatile.class 命令反编译查看字节码文件

这里我们只展示这段代码得字节码:public static volatile int counter = 1;

public static volatile int counter;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE
// 下面为初始化counter时的字节码
0: iconst_2
1: putstatic     #2                  // Field counter:I
4: getstatic     #3                  // Field
  • descriptor:代表参数的类型和修饰符
  • flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE:标志
  • putstatic:对静态属性进行操作

descriptor ,关于字节码的类型对应如下:

B基本类型byte

C基本类型char

D基本类型double

F基本类型float

I基本类型int

J基本类型long

S基本类型short

Z基本类型boolean

V特殊类型void

L对象类型,以分号结尾,如Ljava/lang/Object;

对于数组类型,每一位使用一个前置的[字符来描述,如定义一个java.lang.String[][]类型的维数组,将被记录为[[Ljava/lang/String;

我们后续的操作都可以通过 ACC_VOLATILE 这个标志来知道该变量已被 volatile 所修饰

4.3 HotSpot 源码层面

对于带有 volatile 修饰的变量,我们的 JVM 是怎么去实现的呢?

这里涉及到四个屏障:StoreStore,StoreLoad,LoadStore,LoadLoad

JSR内存屏障:

  • LoadLoad屏障:对于这样的语句Load1;LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:对于这样的语句Store1;StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见
  • LoadStore屏障:对于这样的语句Load1; LoadStore;Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreLoad屏障:对于这样的语句Store1;StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

我们的 JVM 确实是这样实现的,我们一起来看一下具体的实现吧。

Java中,静态属性属于类的。操作静态属性,对应的指令为 putstatic

我们以 openjdk8 根路径 jdk\src\hotspot\share\interpreter\zero 路径下的 bytecodeInterpreter.cpp 文件中,处理 putstatic 指令的代码:

CASE(_putstatic):
    {
          // .... 省略若干行 
          // Now store the result 现在要开始存储结果了
          // ConstantPoolCacheEntry* cache;     -- cache是常量池缓存实例
          // cache->is_volatile()               -- 判断是否有volatile访问标志修饰
          int field_offset = cache->f2_as_index();
          // ****重点判断逻辑**** 
          if (cache->is_volatile()) { 
            // volatile变量的赋值逻辑
            if (tos_type == itos) {
              obj->release_int_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == atos) {// 对象类型赋值
              VERIFY_OOP(STACK_OBJECT(-1));
              obj->release_obj_field_put(field_offset, STACK_OBJECT(-1));
              OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
            } else if (tos_type == btos) {// byte类型赋值
              obj->release_byte_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ltos) {// long类型赋值
              obj->release_long_field_put(field_offset, STACK_LONG(-1));
            } else if (tos_type == ctos) {// char类型赋值
              obj->release_char_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == stos) {// short类型赋值
              obj->release_short_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ftos) {// float类型赋值
              obj->release_float_field_put(field_offset, STACK_FLOAT(-1));
            } else {// double类型赋值
              obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));
            }
            // *** 写完值后的storeload屏障 ***
            OrderAccess::storeload();
          } else {
            // 非volatile变量的赋值逻辑
          }       
  }

这里贴一下 cache->is_volatile() 的源码,路径:jdk\src\hotspot\share\utilities\accessFlags.hpp

// Java access flags
  bool is_public      () const         { return (_flags & JVM_ACC_PUBLIC      ) != 0; }
  bool is_private     () const         { return (_flags & JVM_ACC_PRIVATE     ) != 0; }
  bool is_protected   () const         { return (_flags & JVM_ACC_PROTECTED   ) != 0; }
  bool is_static      () const         { return (_flags & JVM_ACC_STATIC      ) != 0; }
  bool is_final       () const         { return (_flags & JVM_ACC_FINAL       ) != 0; }
  bool is_synchronized() const         { return (_flags & JVM_ACC_SYNCHRONIZED) != 0; }
  bool is_super       () const         { return (_flags & JVM_ACC_SUPER       ) != 0; }
  bool is_volatile    () const         { return (_flags & JVM_ACC_VOLATILE    ) != 0; }
  bool is_transient   () const         { return (_flags & JVM_ACC_TRANSIENT   ) != 0; }
  bool is_native      () const         { return (_flags & JVM_ACC_NATIVE      ) != 0; }
  bool is_interface   () const         { return (_flags & JVM_ACC_INTERFACE   ) != 0; }
  bool is_abstract    () const         { return (_flags & JVM_ACC_ABSTRACT    ) != 0; }

我们看一下赋值 obj->release_long_field_put(field_offset, STACK_LONG(-1)) 的源代码:

jdk\src\hotspot\share\oops\oop.inline.hpp

jlong oopDesc::long_field_acquire(int offset) const                   
	{ 
    	return Atomic::load_acquire(field_addr<jlong>(offset)); 
    }
void oopDesc::release_long_field_put(int offset, jlong value)         
	{ 
    	Atomic::release_store(field_addr<jlong>(offset), value); 
	}

我们前往 jdk\src\hotspot\share\runtime\atomic.hpp看一下 Atomic::release_store 的方法:

inline T Atomic::load_acquire(const volatile T* p) {
  return LoadImpl<T, PlatformOrderedLoad<sizeof(T), X_ACQUIRE> >()(p);
}
template <typename D, typename T>
inline void Atomic::release_store(volatile D* p, T v) {
  StoreImpl<D, T, PlatformOrderedStore<sizeof(D), RELEASE_X> >()(p, v);
}

我们可以清楚的看到,const volatile T* p 和 volatile D* p 在调用的时候,直接使用了C/C++volatile 关键字

我们继续往下看,在我门执行完参数的赋值后,会有这个一个操作:OrderAccess::storeload();

我们观察 jdk\src\hotspot\share\runtimeorderAccess.hpp 文件,发现有这么一段代码:

// barriers 屏障
  static void     loadload();
  static void     storestore();
  static void     loadstore();
  static void     storeload();

  static void     acquire();
  static void     release();
  static void     fence();

我们可以清楚的看到,这就是我们之前提到的 JVM 的读写屏障

当然,我们还要看其在 linux_x86 实现方式,在 jdk\src\hotspot\os_cpu\linux_x86orderAccess_linux_x86.hpp

// A compiler barrier, forcing the C++ compiler to invalidate all memory assumptions
static inline void compiler_barrier() {
  __asm__ volatile ("" : : : "memory");
}

inline void OrderAccess::loadload()   { compiler_barrier(); }
inline void OrderAccess::storestore() { compiler_barrier(); }
inline void OrderAccess::loadstore()  { compiler_barrier(); }
inline void OrderAccess::storeload()  { fence();            }

inline void OrderAccess::acquire()    { compiler_barrier(); }
inline void OrderAccess::release()    { compiler_barrier(); }

4.4 CPU层面

在上面的代码中,我们可以看到,最关键的是这一行代码:__asm__ volatile ("" : : : "memory");

  • __asm__ :用于指示编译器在此插入汇编语句
  • volatile :告诉编译器,严禁将此处的汇编语句与其它的语句重组合优化。即:原原本本按原来的样子处理这这里的汇编。
  • (“” : : : “memory”):memory 强制 gcc 编译器假设 RAM 所有内存单元均被汇编指令修改,这样 cpu 中的 registers 和 cache 中已缓存的内存单元中的数据将作废。cpu 将不得不在需要的时候重新读取内存中的数据。这就阻止了 cpu 又将 registers, cache 中的数据用于去优化指令,而避免去访问内存。

简单概括:告诉我们的CPU,别瞎几把给我优化了,我就要串行执行。

这样我们可以看到,这些指令都是通过更改CPU的 寄存器 和 缓存 来保持有序性的。

还有,我们观察这些方法,会发现有一个叫 fence() 的方法,我们观察一下这个方法:

inline void OrderAccess::fence() {
   // always use locked addl since mfence is sometimes expensive
   // 
#ifdef AMD64
  __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
  __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  compiler_barrier();
}

我们可以看到,我们的方法不建议我们使用我们的原语指令 mfence(内存屏障) ,因为 mfence 的资源消耗要比 locked 资源消耗的多。

补充:(Intel 的原语指令:mfence内存屏障ifence读屏障sfence写屏障)

直接判断是不是 AMD64 来对其不同的寄存器 rsp\esp做处理

“lock; addl $0,0(%%rsp)”:在 rsp 寄存器上加一个 0) 指令是一个 Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个 CPU。

好了,到这里,我们的 volatile 基本差不多了。

五、 hanppens-before原则 与 hanppens-before原则

虽然Java虚拟机和执行系统会对指令进行一定的重排,但是指令重排是有原则的,并非所有的指令都可以随便改变执行位置,以下罗列了一些基本原则,这些原则是指令重排不可违背的。

  • 程序顺序原则:一个线程内保证语义的串行性。
  • volatile规则:volatile变量的写先于读发生,这保证了volatile变量的可见性。
  • 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前。
  • 传递性:A先于B,B先于C,那么A必然先于C。
  • 线程启动规则 :线程的start()方法先于它的每一个动作。
  • 线程终止规则 :线程的所有操作先于线程的终结(Thread.join())。
  • 线程中断规则 :线程的中断(interrupt())先于被中断线程的代码。
  • 对象终结规则 :对象的构造函数的执行、结束先于finalize()方法。

这些原则都是为了保证指令重排不会破坏原有的语义结构。

as if serial 表达的就是:不管如何重排序,单线程执行的结果不会改变。

ok,结束!