jmm内存模型

一个cpu下有多个核心,每个核心有自己的L1,L2级缓存,所有核公用一个L3级缓存
在这里插入图片描述
因为存在CPU缓存一致性协议(例如MESI),多个CPU核心之间缓存不会出现不同步的问题(L1L2L3是同步的),即不存在内存可见性问题。
缓存一致性协议对性能有很大损耗,为了解决这个问题,又进行了各种优化。例如,在计算单元和
L1之间加了Store Buffer、Load Buffer(还有其他各种Buffer)
在这里插入图片描述

L1、L2、L3和主内存之间是同步的,有缓存一致性协议的保证,但是Store Buffer、Load Buffer和
L1之间却是异步的。向内存中写入一个变量,这个变量会保存在Store Buffer里面,稍后才异步地写入
L1中,同时同步写入主内存中。

重排序和内存可见性的关系
Store Buffer的延迟写入是重排序的一种,称为内存重排序(Memory Ordering)。比如上图中有写入和读取两个指令,分别在core0和core1中运行,写入指令向store buffer里面写入,然后store buffer向L3缓存中写入,读取指令读数据的时候,通过load buffer来读取L3的数据。本来是写入先运行完,读取后运行完,因为有buffer的原因导致读取指令通过load buffer先读取了L3的数据。、

重排序类型:
  1. 编译器重排序。
    对于没有先后依赖关系的语句,编译器可以重新调整语句的执行顺序。
    编译的时候优化,编译器将没有先后依赖关系的语句顺序调整,提高代码效率。
  2. CPU指令重排序。
    在指令级别,让没有依赖关系的多条指令并行。
  3. CPU内存重排序。(造成内存可见性的主因)
    CPU有自己的缓存,指令的执行顺序和写入主内存的顺序不完全一致。

内存屏障
为了禁止编译器重排序和 CPU 重排序,在编译器和 CPU 层面都有对应的指令,也就是内存屏障
(Memory Barrier)。
编译器的内存屏障,只是为了告诉编译器不要对指令进行重排序。当编译完成之后,这种内存屏障
就消失了,CPU并不会感知到编译器中内存屏障的存在。
而CPU的内存屏障是CPU提供的指令,可以由开发者显式调用。

基本的CPU内存屏障大致可以分成四种:

  1. LoadLoad:禁止读和读的重排序。
  2. StoreStore:禁止写和写的重排序。
  3. LoadStore:禁止读和写的重排序。
  4. StoreLoad:禁止写和读的重排序。

Java在Unsafe类中提供了三个内存屏障函数

public final class Unsafe { 
	// ... 
	//这里的内存屏障函数都是本地方法
	// loadFence=LoadLoad+LoadStore 读读和读写不重排
	public native void loadFence(); 
	// storeFence=StoreStore+LoadStore 写写和读写不重排
	public native void storeFence(); 
	// fullFence=LoadLoad+LoadStore+StoreStore+StoreLoad 全部不重排
	public native void fullFence(); 
	// ... 
}
as-if-serial

as-if-serial 语义(就像是序列化执行了一样)
1.单线程程序的重排序规则
无论什么语言,站在编译器和CPU的角度来说,不管怎么重排序,单线程程序的执行结果不能改
变,这就是单线程程序的重排序规则。
即只要操作之间没有数据依赖性,编译器和CPU都可以任意重排序,因为执行结果不会改变,代码
看起来就像是完全串行地一行行从头执行到尾,这也就是as-if-serial语义。
对于单线程程序来说,编译器和CPU可能做了重排序,但开发者感知不到,也不存在内存可见性问
题。
2.多线程程序的重排序规则
编译器和CPU的这一行为对于单线程程序没有影响,但对多线程程序却有影响。
对于多线程程序来说,线程之间的数据依赖性太复杂,编译器和CPU没有办法完全理解这种依赖性
并据此做出最合理的优化。
编译器和CPU只能保证每个线程的as-if-serial语义。
线程之间的数据依赖和相互影响,需要编译器和CPU的上层来确定。
上层要告知编译器和CPU在多线程场景下什么时候可以重排序,什么时候不能重排序。

使用happen-before描述两个操作之间的内存可见性。

java内存模型(JMM)是一套规范,在多线程中,一方面,要让编译器和CPU可以灵活地重排序;
另一方面,要对开发者做一些承诺,明确告知开发者不需要感知什么样的重排序,需要感知什么样的重
排序。然后,根据需要决定这种重排序对程序是否有影响。如果有影响,就需要开发者显示地通过
volatile、synchronized等线程同步机制来禁止重排序。

关于happen-before:

如果A happen-before B,意味着A的执行结果必须对B可见,也就是保证跨线程的内存可见性。A happen before B不代表A一定在B之前执行。因为,对于多线程程序而言,两个操作的执行顺序是不确定的。happen-before只确保如果A在B之前执行,则A的执行结果必须对B可见。定义了内存可见性的约束,也就定义了一系列重排序的约束。
基于happen-before的这种描述方法,JMM对开发者做出了一系列承诺:

  1. 单线程中的每个操作,happen-before对应该线程中任意后续操作(也就是 as-if-serial语义保证)。
  2. 对volatile变量的写入,happen-before对应后续对这个变量的读取。
  3. 对synchronized的解锁,happen-before对应后续对这个锁的加锁。

    JMM对编译器和CPU 来说,volatile变量不能重排序;非 volatile变量可以任意重排序。

happen-before还具有传递性,即若A happen-before B,B happen-before C,则A happen-before C。

/**
*假设线程A先调用了set,设置了a=5;之后线程B调用了get,返回值一定是a=5。为什么呢?
*操作1和操作2是在同一个线程内存中执行的
*操作1 happen-before 操作2
*同理,操作3 happen- before操作4。
*又因为c是volatile变量,对c的写入happen-before对c的读取,所以操作2 happen- before操作3。
*利用happen-before的传递性,就得到:
*操作1 happen-before 操作2 happen-before 操作3 happen-before操作4。 
*所以,操作1的结果,一定对操作4可见。
*/
class A {
    private int a = 0;
    private volatile int c = 0;
    public void set() {
		a=5;		// 操作1
		c=1;		// 操作2 
	}
	public int get() { 
		int d = c;  // 操作3 
		return a; 	// 操作4
	} 
}

synchronized同样具有happen-before语义。

class A {
   	private int a = 0;
    private int c = 0;
    public synchronized void set() {
		a=5;// 操作1
		c=1;// 操作2 
	}
    public synchronized int get() {
        return a;
	} 
}

假设线程A先调用了set,设置了a=5;之后线程B调用了get,返回值也一定是a=5。

线程A:
	加锁; 	// 操作1
	a=5;	// 操作2 
	c=1;	// 操作3 
	解锁; 	// 操作4
线程B:
	加锁; 	// 操作5
	读取a; 	// 操作6 
	解锁; 	// 操作7

操作1happen-before操作2…happen-before操作7。所以,a、c都不是volatile变量,但仍然有内存可见性。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值