#Java#互联网大厂面试必问的Java内存模型以及并发编程( 五 )


虽然 Java 规范允许上面的实现 , 但商用虚拟机中基本都采用了原子性的操作 , 因此在日常使用中几乎不会出现读取到“半个变量”的情况 , so , 这个了解下就行 。
先行发生原则先行发生(happens-before)是 Java 内存模型中定义的两项操作之间的偏序关系 , 如果操作A 先行发生于操作B , 那么A的结果对B可见 。 happens-before关系的分析需要分为单线程和多线程的情况:

  • 单线程下的 happens-before 字节码的先后顺序天然包含happens-before关系:因为单线程内共享一份工作内存 , 不存在数据一致性的问题 。 在程序控制流路径中靠前的字节码 happens-before 靠后的字节码 , 即靠前的字节码执行完之后操作结果对靠后的字节码可见 。 然而 , 这并不意味着前者一定在后者之前执行 。 实际上 , 如果后者不依赖前者的运行结果 , 那么它们可能会被重排序 。
  • 多线程下的 happens-before 多线程由于每个线程有共享变量的副本 , 如果没有对共享变量做同步处理 , 线程1更新执行操作A共享变量的值之后 , 线程2开始执行操作B , 此时操作A产生的结果对操作B不一定可见 。
为了方便程序开发 , Java 内存模型实现了下述的先行发生关系:
  • 程序次序规则: 一个线程内 , 按照代码顺序 , 书写在前面的操作先行发生于书写在后面的操作 。
  • 管程锁定规则: 一个unLock操作先行发生于后面对同一个锁的lock操作 。
  • volatile变量规则: 对一个变量的写操作 happens-before 后面对这个变量的读操作 。
  • 传递规则: 如果操作A 先行发生于操作B , 而操作B又先行发生于操作C , 则可以得出操作A 先行发生于操作C 。
  • 线程启动规则: Thread对象的start()方法先行发生于此线程的每个一个动作 。
  • 线程中断规则: 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生 。
  • 线程终结规则: 线程中所有的操作都先行发生于线程的终止检测 , 我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行 。
  • 对象终结规则: 一个对象的初始化完成先行发生于它的 finalize()方法的开始
内存屏障上边的一系列操作保证了数据一致性 , Java中如何保证底层操作的有序性和可见性?可以通过内存屏障 。
内存屏障是被插入两个 CPU 指令之间的一种指令 , 用来禁止处理器指令发生重排序(像屏障一样) , 从而保障有序性的 。 另外 , 为了达到屏障的效果 , 它也会使处理器写入、读取值之前 , 将主内存的值写入高速缓存 , 清空无效队列 , 从而保障可见性 。
eg:
Store1;Store2;Load1;StoreLoad;  //内存屏障Store3;Load2;Load3;

对于上面的一组 CPU 指令(Store表示写入指令 , Load表示读取指令) , StoreLoad 屏障之前的 Store 指令无法与StoreLoad 屏障之后的 Load 指令进行交换位置 , 即重排序 。 但是 StoreLoad 屏障之前和之后的指令是可以互换位置的 , 即 Store1 可以和 Store2 互换 , Load2 可以和 Load3 互换 。
常见的 4 种屏障