java内存模型 内存屏障

一些专业名词释义:

屏障:1:泛指遮蔽、阻挡之物。2:保护,遮蔽。

内存屏障:保护遮蔽内存

内存屏障指令:保护遮蔽内存的指令。

栅栏:1.用铁条或木条等做成的类似篱笆而较坚固的东西。2:比喻障碍,隔阂。

主存:堆内存就是Java对象所使用的区域,在JMM的定义中通常把这块空间叫作“Main Memroy”(主存)

操作数栈:Java虚拟机的解释执行引擎被称为”基于栈的执行引擎”,其中所指的栈就是指-操作数栈。操作数栈也常被称为操作栈。和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。

工作内存:栈空间内部应当包含局部变量、操作数栈、当前方法的常量池指针、当前方法的返回地址等信息,这块空间在我们看来是最接近CPU运算的,也是每个线程私有的空间,当调用某方法开始时将给该私有栈分配空间,在方法内部再调用方法还会继续使用相应的栈空间,方法返回时回收相应的栈空间(不论是否抛出异常)。这块空间通常叫作“Working Memory”(工作内存)。

read/write:工作内存与主存之间会采用read/write的方式进行通信。read:将主存读入工作内存。write:将工作内存写入主存。

load/store操作:当工作内存中的数据需要计算时,它会发生load/store操作,load操作通常是将本地变量推至栈顶,用来给CPU调度运算,而store就是将栈顶的数据写入本地变量。

局部变量(本地变量)在使用时通常不存在一致性问题,因为它的定义本身就归属于线程运行时,生命周期由相应的代码块决定。所以在一致性问题上,我们关注的是多线程对主存的一些数据读写操作。

JMM中一些普通变量的操作指令:
◎ Load操作发生在read之后(两个之间可以有其他的指令)。
◎ 普通变量的修改未必会立即发生Store操作,但发生Store操作,就会发生write操作。
有了这个基本规则后,我们似乎就不太关注write/read了,因为Load发生之前肯定有read,而Store操作之后肯定有write,因此我们将读/写的4个步骤理解为简单的2个步骤。

StoreLoad的意思:
我们可以简单地认为让Store先于Load发生。例如两个在某个瞬间同时修改和读取主存中的一个共享变量,此时的读取操作将发生在修改之后。有了这样一种特征,就实现了最细粒度的锁,也是最轻量级的锁。这样的方式仅仅能保证读的一瞬间确保线程读取到最新的数据,因此要进一步做到读取、修改、写入的动作是一致的,就将其升级为原子性。要达到原子性的效果,可以通过可见性、CAS自旋来完成,也可以通过synchronized来完成。

——————————————————————————————————————————————

编译器和处理器必须同时遵守重排规则。由于单核处理器能确保与“顺序执行”相同的一致性,所以在单核处理器上并不需要专门做什么处理,就可以保证正确的执行顺序。但在多核处理器上通常需要使用内存屏障指令来确保这种一致性。即使编译器优化掉了一个字段访问(例如,因为一个读入的值未被使用),这种情况下还是需要产生内存屏障,就好像这个访问仍然需要保护。(可以参考下面的优化掉内存屏障的章节)。

内存屏障仅仅与内存模型中“获取”、“释放”这些高层次概念有间接的关系。内存屏障并不是“同步屏障”,内存屏障也与在一些垃圾回收机制中“写屏障(write barriers)”的概念无关。内存屏障指令仅仅直接控制CPU与其缓存之间,CPU与其准备将数据写入主存或者写入等待读取、预测指令执行的缓冲中的写缓冲之间的相互操作。这些操作可能导致缓冲、主内存和其他处理器做进一步的交互。但在JAVA内存模型规范中,没有强制处理器之间的交互方式,只要数据最终变为全局可用,就是说在所有处理器中可见,并当这些数据可见时可以获取它们。

 

内存屏障的种类

几乎所有的处理器至少支持一种粗粒度的屏障指令,通常被称为“栅栏(Fence)”,它保证在栅栏前初始化的load和store指令,能够严格有序的在栅栏后的load和store指令之前执行。无论在何种处理器上,这几乎都是最耗时的操作之一(与原子指令差不多,甚至更消耗资源),所以大部分处理器支持更细粒度的屏障指令。

内存屏障的一个特性是将它们运用于内存之间的访问。尽管在一些处理器上有一些名为屏障的指令,但是正确的/最好的屏障使用取决于内存访问的类型。下面是一些屏障指令的通常分类,正好它们可以对应上常用处理器上的特定指令(有时这些指令不会导致操作)。

LoadLoad 屏障

序列:Load1,Loadload,Load2

确保Load1所要读入的数据能够在被Load2和后续的load指令访问前读入。通常能执行预加载指令或/和支持乱序处理的处理器中需要显式声明Loadload屏障,因为在这些处理器中正在等待的加载指令能够绕过正在等待存储的指令。 而对于总是能保证处理顺序的处理器上,设置该屏障相当于无操作。

StoreStore  屏障

序列:Store1,StoreStore,Store2

确保Store1的数据在Store2以及后续Store指令操作相关数据之前对其它处理器可见(例如向主存刷新数据)。通常情况下,如果处理器不能保证从写缓冲或/和缓存向其它处理器和主存中按顺序刷新数据,那么它需要使用StoreStore屏障。

LoadStore 屏障

序列: Load1; LoadStore; Store2

确保Load1的数据在Store2和后续Store指令被刷新之前读取。在等待Store指令可以越过loads指令的乱序处理器上需要使用LoadStore屏障。

StoreLoad Barriers

序列: Store1; StoreLoad; Load2

确保Store1的数据在被Load2和后续的Load指令读取之前对其他处理器可见。StoreLoad屏障可以防止一个后续的load指令 不正确的使用了Store1的数据,而不是另一个处理器在相同内存位置写入一个新数据。正因为如此,所以在下面所讨论的处理器为了在屏障前读取同样内存位置存过的数据,必须使用一个StoreLoad屏障将存储指令和后续的加载指令分开。Storeload屏障在几乎所有的现代多处理器中都需要使用,但通常它的开销也是最昂贵的。它们昂贵的部分原因是它们必须关闭通常的略过缓存直接从写缓冲区读取数据的机制。这可能通过让一个缓冲区进行充分刷新(flush),以及其他延迟的方式来实现。

在下面讨论的所有处理器中,执行StoreLoad的指令也会同时获得其他三种屏障的效果。所以StoreLoad可以作为最通用的(但通常也是最耗性能)的一种Fence。(这是经验得出的结论,并不是必然)。反之不成立,为了达到StoreLoad的效果而组合使用其他屏障并不常见。

下表显示这些屏障如何符合JSR-133排序规则。

需要的屏障 第二步
第一步Normal LoadNormal StoreVolatile Load
MonitorEnter
Volatile Store
MonitorExit
Normal Load   LoadStore
Normal Store   StoreStore
Volatile Load
MonitorEnter
LoadLoadLoadStoreLoadLoadLoadStore
Volatile Store
MonitorExit
  StoreLoadStoreStore

另外,特殊的final字段规则在下列代码中需要一个StoreStore屏障

x.finalField = v; StoreStore; sharedRef = x;

如下例子解释如何放置屏障:

class X {
	int a, b;
	volatile int v, u;

	void f() {
		int i, j;

		i = a;// load a
		j = b;// load b
		i = v;// load v
		// LoadLoad
		j = u;// load u
		// LoadStore
		a = i;// store a
		b = j;// store b
		// StoreStore
		v = i;// store v
		// StoreStore
		u = j;// store u
		// StoreLoad
		i = u;// load u
		// LoadLoad
		// LoadStore
		j = b;// load b
		a = i;// store a
	}
}

数据依赖和屏障

一些处理器为了保证依赖指令的交互次序需要使用LoadLoad和LoadStore屏障。在一些(大部分)处理器中,一个load指令或者一个依赖于之前加载值的store指令被处理器排序,并不需要一个显式的屏障。这通常发生于两种情况,间接取值(indirection):

Load x; Load x.field

和条件控制(control)

Load x; if (predicate(x)) Load or Store y;

但特别的是不遵循间接排序的处理器,需要为final字段设置屏障,使它能通过共享引用访问最初的引用。

x = sharedRef; … ; LoadLoad; i = x.finalField;

相反的,如下讨论,确定遵循数据依赖的处理器,提供了几个优化掉LoadLoad和LoadStore屏障指令的机会。(尽管如此,在任何处理器上,对于StoreLoad屏障不会自动清除依赖关系)。

与原子指令交互

屏障在不同处理器上还需要与MonitorEnter和MonitorExit实现交互。锁或者解锁通常必须使用原子条件更新操作CompareAndSwap(CAS)指令或者LoadLinked/StoreConditional (LL/SC),就如执行一个volatile store之后紧跟volatile load的语义一样。CAS或者LL/SC能够满足最小功能,一些处理器还提供其他的原子操作(如,一个无条件交换),这在某些时候它可以替代或者与原子条件更新操作结合使用。

在所有处理器中,原子操作可以避免在正被读取/更新的内存位置进行写后读(read-after-write)。(否则标准的循环直到成功的结构体(loop-until-success )没有办法正常工作)。但处理器在是否为原子操作提供比隐式的StoreLoad更一般的屏障特性上表现不同。一些处理器上这些指令可以为MonitorEnter/Exit原生的生成屏障;其它的处理器中一部分或者全部屏障必须显式的指定。

为了分清这些影响,我们必须把Volatiles和Monitors分开:

需要的屏障 第二步
第一步Normal LoadNormal StoreVolatile LoadVolatile StoreMonitorEnterMonitorExit
Normal Load   LoadStore LoadStore
Normal Store   StoreStore StoreExit
Volatile LoadLoadLoadLoadStoreLoadLoadLoadStoreLoadEnterLoadExit
Volatile Store  StoreLoadStoreStoreStoreEnterStoreExit
MonitorEnterEnterLoadEnterStoreEnterLoadEnterStoreEnterEnterEnterExit
MonitorExit  ExitLoadExitStoreExitEnterExitExit

另外,特殊的final字段规则需要一个StoreLoad屏障。

x.finalField = v; StoreStore; sharedRef = x;

在这张表里,”Enter”与”Load”相同,”Exit”与”Store”相同,除非被原子指令的使用和特性覆盖。特别是:

  • EnterLoad 在进入任何需要执行Load指令的同步块/方法时都需要。这与LoadLoad相同,除非在MonitorEnter时候使用了原子指令并且它本身提供一个至少有LoadLoad属性的屏障,如果是这种情况,相当于没有操作。
  • StoreExit在退出任何执行store指令的同步方法块时候都需要。这与StoreStore一致,除非MonitorExit使用原子操作,并且提供了一个至少有StoreStore属性的屏障,如果是这种情况,相当于没有操作。
  • ExitEnter和StoreLoad一样,除非MonitorExit使用了原子指令,并且/或者MonitorEnter至少提供一种屏障,该屏障具有StoreLoad的属性,如果是这种情况,相当于没有操作。

在编译时不起作用或者导致处理器上不产生操作的指令比较特殊。例如,当没有交替的load和store指令时,EnterEnter用于分离嵌套的MonitorEnter。下面这个例子说明如何使用这些指令类型:

class X {
	int a;
	volatile int v;

	void f() {
		int i;
		synchronized (this) { // enter EnterLoad EnterStore
			i = a;// load a
			a = i;// store a
		}// LoadExit StoreExit exit ExitEnter

		synchronized (this) {// enter ExitEnter
			synchronized (this) {// enter
			}// EnterExit exit
		}// ExitExit exit ExitEnter ExitLoad

		i = v;// load v

		synchronized (this) {// LoadEnter enter
		} // exit ExitEnter ExitStore

		v = i; // store v
		synchronized (this) { // StoreEnter enter
		} // EnterExit exit
	}

}

Java层次的对原子条件更新的操作将在JDK1.5中发布(JSR-166),因此编译器需要发布相应的代码,综合使用上表中使用MonitorEnter和MonitorExit的方式,——从语义上说,有时在实践中,这些Java中的原子更新操作,就如同他们都被锁所包围一样。


原文:http://gee.cs.oswego.edu/dl/jmm/cookbook.html 第二节

转载自:http://ifeve.com/jmm-cookbook-mb/

参考:

http://blog.jobbole.com/53697/

http://www.infoq.com/cn/articles/memory_barriers_jvm_concurrency

    原文作者:java内存模型
    原文地址: https://blog.csdn.net/coslay/article/details/41955899
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞