as-if-serial属性

菩提本无树, 何处惹尘埃。

即时编译器(和处理器)需要保证程序能够遵守as-if-serial属性。通俗地说, 就是在单线程情况下, 要给程序一个顺序执行的假象。即经过重排序的执行结果要与顺序执行的结果一致。

另外, 若两个操作之间存在数据依赖, 那么及时编译器(和处理器)不能调整它们的顺序, 否则将会造成程序语义的改变。

happens-before 关系

问渠那得清如许? 唯有源头活水来。

描述两个操作的内存可见性的。如果操作X happens-before 操作Y, 那么X的结果对于Y可见(Y观测X的结果)

  • Java内存模型定义的线程间happens-before关系

1。解锁操作 happens-before 对该所锁的加锁操作(时序)
2。volatile字段的写操作 happens-before 对该volatile字段的读操作(时序)
3。线程的启动(start) happens-before 线程的第一个操作
4。线程的最后一个操作 happens-before 线程的终止事件(即其他线程通过Thread.isAlive() 或 Thread.join()判断该线程是否中止)
5。线程对其他线程的中断操作 happens-before 被中断线程收到的中断事件(即被中断线程的InterruptedException异常, 或者第三个线程针对被中断线程的Thread.interrupted 或者 Thread.isinterrupted调用)
6。构造器中的最后一个操作 happens-before 析构器的第一个操作

另外happens-before关系还具备传递性.

Java内存模型的底层实现

Java内存模型是通过内存屏障(memory barrier)来禁止重排序的.

对于即使编译器来说, 它会针对每个happens-before 关系, 向正在编译的目标方法中插入相应的读读,读写,写读,写写内存屏障。

这些内存屏障会限制即时编译器的重排序操作。以volatile字段访问为例, 所插入的内存屏障将不允许volatile字段写操作之前的内存访问被重排序至其后; 也将不允许bolatile字段读操作之后的内存访问被重排序至之前。volatile字段的内存屏障是什么类型?

即时编译器会根据具体的底层体系架构, 将内存屏障替换成具体的CPU指令。以我接触最多的X86_64架构来说, 读读,读写以及写写内存屏障是空操作(no-op), 只有写读内存屏障会被替换成具体指令

这样理解重排序

Java代码本应该按源码顺序执行, 但是编译器对部分代码会进行优化, 提高执行效率(cpu 或 内存)

然而, 禁止重排序是处于某种目的(业务逻辑/happens-before)开发人员对编译器的代码重排序进行一定的限制。

对于即时编译器来说, 它会针对happens-before关系, 向正在编译的目标方法中插入相应的内存屏障(读读,读写,写读,写写)

  • volatile
    volatile字段写操作之后的写读内存屏障需要用具体指令来替代.(HotSpot 所选取的具体指令是 lock add DWORD PTR[rsp],0x0, 而非mfence[3].)
    在具体指令的效果, 可以简单理解为强制刷新处理器的写缓存。写缓存是处理器用来加速内存存储效率的一项技术。
    在碰到内存写操作时, 处理器并不会等待该指令结束, 而是直接开始下一指令, 并且依赖于写缓存将更改的数据同步至主内存之中。
    强制刷新写缓存,将使得当前线程写入volatile字段的值(以及写缓存中已有的其他内存修改), 同步至主内存之中。
    强制刷新写缓存, 将是得当前现成写入volatile字段的值(以及写缓存中已有的其他内存修改), 同步至主内存之中。
    由于内存写操作(强制刷新缓存)同时会无效化其他处理器所持有的指向同一内存地址的缓存行, 因此可以认为其他处理器能够立即见到该volatile字段的最新值。


  • 前面提到锁具备happens-before关系。具体来说, 解锁操作happens-before之后对同一把锁的枷锁操作。实际上, 在解锁时, Java虚拟机同样需要强制刷新缓存, 使得当前线程所修改的内存对其他线程可见。
    锁操作具备happens-before关系。具体来说, 解锁操作happens-before之后对同一把锁。也就是说, 如果编译器能够(通过逃逸分析)证明某把锁仅被同一线程持有, 那么它可以移除相应的加锁解锁操作。

  • final
    final实例字段涉及新建对象的发布问题。当一个对象包含final实例字段时, 我们希望其他线程智能看到已初始化的实例字段。

因此, 即时编译器会在final字段的写操作后插入一个写写屏障, 以防某些优化将新建对象的发布(即将实例对象写入一个共享引用中) 重排序至final字段的写操作之前。在X86_64平台上, 写写屏障是空操作

新建对象的安全发布问题(safe publication)问题不仅仅包括final实例字段的可见性, 还包括其他实例字段的可见性。

https://vlkan.com/blog/post/2014/02/14/java-safe-publication/