JVM笔记9 Java内存模型与volatile

硬件效率与一致性

缓存一致性

CPU速度和内存速度间有数量级的差异[1],因此在CPU和内存间需要有高速缓存,但是会带来缓存一致性的问题。
在这里插入图片描述
[1] intel cpu 4 GHz,DDR 3200内存,但是内存芯片速率仅为400 MHz,时钟速度之比为10:1,当处理器需要处于内部高速缓存之外的数据项时,每个周期必须等待10个时钟周期才能使内存芯片完成数据的提取和发送。

乱序执行

为了使处理器内部的运算单元能尽量被充分利用, 处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution) 优化, 处理器会在计算之后将乱序执行的结果重组, 保证该结果与顺序执行的结果是一致的, 但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致, 因此如果存在一个计算任务依赖另外一个计算任务的中间结果, 那么其顺序性并不能靠代码的先后顺序来保证。 与处理器的乱序执行优化类似, Java虚拟机的即时编译器中也有指令重排序(Instruction Reorder) 优化。

java内存模型

为什么有JMM

每个cpu或者操作系统提供的内存模型并不一致,JMM统一内存模型,不然有可能在某些平台上的正常的程序到另外的平台上就不正常了。

JSR-133

JSR133.pdf

JMM内容

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • java内存模型的主要目的是定义程序中各种变量的访问规则, 即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。这里的变量指的是java语言中的实例字段、 静态字段和构成数组对象的元素, 但是不包括局部变量与方法参数。因为局部变量和方法参数是线程私有,不会有数据竞争问题。
  • Java内存模型规定了所有的变量都存储在主内存(Main Memory) 中。
  • 每条线程还有自己的工作内存(Working Memory, 可与前面讲的处理器高速缓存类比) , 线程的工作内存中保存了被该线程使用的变量的主内存副本, 线程对变量的所有操作(读取、 赋值等) 都必须在工作内存中进行, 而不能直接读写主内存中的数据。
  • 不同的线程之间也无法直接访问对方工作内存中的变量, 线程间变量值的传递均需要通过主内存来完成。

volatile

  • 保证此变量对所有线程的可见性
  • 禁止指令重排序

特征

  • 原子性:
  • 可见性:volatile和synchronized和final
  • 有序性:如果在本线程内观察, 所有的操作都是有序的; 如果在一个线程中观察另一个线程,所有的操作都是无序的。 前半句是指“线程内似表现为串行的语义”(Within-Thread As-If-SerialSemantics) , 后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

理解

从原子性,可见性和有序性的角度分析,声明为volatile字段的作用相当于一个类通过get/set同步方法保护普通字段,如下:

final class VFloat {
    private float value;
    final synchronized void set(float f){ 
    	value = f; 
    }
    final synchronized float get(){
	    return value;
	}
}

与使用synchronized相比,声明一个volatile字段的区别在于没有涉及到锁操作。
对volatile字段进行“++”这样的读写操作不会被当做原子操作执行。
有序性和可见性仅对volatile字段进行一次读取或更新操作起作用。
声明一个引用变量为volatile,不能保证通过该引用变量访问到的非volatile变量的可见性。
声明一个数组变量为volatile不能确保数组内元素的可见性。

实现原理

为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。因为内存屏障是一组处理器指令,它并不由JVM直接暴露,因此JVM会根据不同的操作系统插入不同的指令以达成我们所要内存屏障效果。

为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM把内存屏障指令分为下列四类:
在这里插入图片描述
下面是基于保守策略的JMM内存屏障插入策略:
在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。
在每个volatile读操作的后面插入一个LoadStore屏障。

上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。由于不同的处理器有不同“松紧度”的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。

先行发生原则 没明白

如果Java内存模型中所有的有序性都仅靠volatile和synchronized来完成, 那么有很多操作都将会变得非常啰嗦, 但是我们在编写Java并发代码的时候并没有察觉到这一点, 这是因为Java语言中有一个“先行发生”(Happens-Before) 的原则。

// 以下操作在线程A中执行
i = 1;
// 以下操作在线程B中执行
j = i;
// 以下操作在线程C中执行
i = 2;

Java内存模型下一些“天然的”先行发生关系, 这些先行发生关系无须任何同步器协助就已经存在, 可以在编码中直接使用。 如果两个操作之间的关系不在此列, 并且无法从下列规则推导出来, 则它们就没有顺序性保障, 虚拟机可以对它们随意地进行重排序。

  • 程序次序规则(Program Order Rule) : 在一个线程内, 按照控制流顺序, 书写在前面的操作先行发生于书写在后面的操作。 注意, 这里说的是控制流顺序而不是程序代码顺序, 因为要考虑分支、 循环等结构。
  • 管程锁定规则(Monitor Lock Rule) : 一个unlock操作先行发生于后面对同一个锁的lock操作。 这里必须强调的是“同一个锁”, 而“后面”是指时间上的先后。
  • volatile变量规则(Volatile Variable Rule) : 对一个volatile变量的写操作先行发生于后面对这个变量的读操作, 这里的“后面”同样是指时间上的先后。·线程启动规则(Thread Start Rule) : Thread对象的start()方法先行发生于此线程的每一个动作。
  • 线程终止规则(Thread Termination Rule) : 线程中的所有操作都先行发生于对此线程的终止检测, 我们可以通过Thread::join()方法是否结束、 Thread::isAlive()的返回值等手段检测线程是否已经终止执行。
  • 线程中断规则(Thread Interruption Rule) : 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生, 可以通过Thread::interrupted()方法检测到是否有中断发生。
  • 对象终结规则(Finalizer Rule) : 一个对象的初始化完成(构造函数执行结束) 先行发生于它的finalize()方法的开始。
  • 传递性(Transitivity) : 如果操作A先行发生于操作B, 操作B先行发生于操作C, 那就可以得出操作A先行发生于操作C的结论。

参考博客

https://blog.csdn.net/zjcjava/article/details/78406330
http://ifeve.com/
https://www.cnblogs.com/xrq730/p/7048693.html
https://blog.csdn.net/zezezuiaiya/article/details/81456060
https://www.cnblogs.com/chenssy/p/6379280.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值