Open wittyResry opened 6 years ago
Java内存模型作用? Java包含了几个语言级别的关键字,包括:volatile, final以及synchronized,目的是为了帮助程序员向编译器描述一个程序的并发需求。Java内存模型定义了volatile和synchronized的行为,更重要的是保证了同步的java程序在所有的处理器架构下面都能正确的运行。
这意味着:任何内存操作,这个内存操作在退出一个同步块前对一个线程是可见的,对任何线程在它进入一个被相同的监视器保护的同步块后都是可见的,因为所有内存操作happens before释放监视器以及释放监视器happens before获取监视器。
Java内存模型(JMM, Java Memory Model)主要是由禁止指令重排的规则所组成的,其中包括了字段(包括数组中的元素)的存取指令和监视器(锁)的控制指令。
能否重排 | 第二个操作 | ||
---|---|---|---|
第一个操作 | Normal Load/Normal Store | Volatile load/MonitorEnter | Volatile store/MonitorExit |
Normal Load/Normal Store | No | ||
Volatile load/MonitorEnter | No | No | No |
Volatile store/MonitorExit | No | No |
上表:空白的单元格代表在不违反Java的基本语义下的重排是允许的 术语说明: Normal Load指令包括:对非volatile字段的读取,getfield,getstatic和array load; Normal Store指令包括:对非volatile字段的存储,putfield,putstatic和array store; Volatile load指令包括:对多线程环境的volatile变量的读取,getfield,getstatic; Volatile store指令包括:对多线程环境的volatile变量的存储,putfield,putstatic; MonitorEnters指令(包括进入同步块synchronized方法)是用于多线程环境的锁对象; MonitorExits指令(包括离开同步块synchronized方法)是用于多线程环境的锁对象。
编译器和处理器必须同时遵守重排规则。由于单核处理器能确保与“顺序执行”相同的一致性,所以在单核处理器上并不需要专门做什么处理,就可以保证正确的执行顺序。但在多核处理器上通常需要使用内存屏障指令来确保这种一致性。
内存屏障指令仅仅直接控制CPU与其缓存之间,CPU与其准备将数据写入主存或者写入等待读取、预测指令执行的缓冲中的写缓冲之间的相互操作。这些操作可能导致缓冲、主内存和其他处理器做进一步的交互。但在JAVA内存模型规范中,没有强制处理器之间的交互方式,只要数据最终变为全局可用,就是说在所有处理器中可见,并当这些数据可见时可以获取它们。
栅栏(Fence):它保证在栅栏前初始化的load和store指令,能够严格有序的在栅栏后的load和store指令之前执行。无论在何种处理器上,这几乎都是最耗时的操作之一(与原子指令差不多,甚至更消耗资源),所以大部分处理器支持更细粒度的屏障指令。
序列:Load1,Loadload,Load2
确保Load1所要读入的数据能够在被Load2和后续的load指令访问前读入。通常能执行预加载指令或/和支持乱序处理的处理器中需要显式声明Loadload屏障,因为在这些处理器中正在等待的加载指令能够绕过正在等待存储的指令。 而对于总是能保证处理顺序的处理器上,设置该屏障相当于无操作。
序列:Store1,StoreStore,Store2
确保Store1的数据在Store2以及后续Store指令操作相关数据之前对其它处理器可见(例如向主存刷新数据)。通常情况下,如果处理器不能保证从写缓冲或/和缓存向其它处理器和主存中按顺序刷新数据,那么它需要使用StoreStore屏障。
序列: Load1; LoadStore; Store2
确保Load1的数据在Store2和后续Store指令被刷新之前读取。在等待Store指令可以越过loads指令的乱序处理器上需要使用LoadStore屏障。
序列: Store1; StoreLoad; Load2
确保Store1的数据在被Load2和后续的Load指令读取之前对其他处理器可见。StoreLoad屏障可以防止一个后续的load指令 不正确的使用了Store1的数据,而不是另一个处理器在相同内存位置写入一个新数据。正因为如此,所以在下面所讨论的处理器为了在屏障前读取同样内存位置存过的数据,必须使用一个StoreLoad屏障将存储指令和后续的加载指令分开。Storeload屏障在几乎所有的现代多处理器中都需要使用,但通常它的开销也是最昂贵的。它们昂贵的部分原因是它们必须关闭通常的略过缓存直接从写缓冲区读取数据的机制。这可能通过让一个缓冲区进行充分刷新(flush),以及其他延迟的方式来实现。
需要的屏障 | 第二步 | |||
---|---|---|---|---|
第一步 | Normal Load | Normal Store | Volatile LoadMonitorEnter | Volatile StoreMonitorExit |
Normal Load | LoadStore | |||
Normal Store | StoreStore | |||
Volatile LoadMonitorEnter | LoadLoad | LoadStore | LoadLoad | LoadStore |
Volatile StoreMonitorExit | StoreLoad | StoreStore |
如下例子解释如何放置屏障:
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
}
}
屏障在不同处理器上还需要与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 Load | Normal Store | Volatile Load | Volatile Store | MonitorEnter | MonitorExit |
Normal Load | LoadStore | LoadStore | ||||
Normal Store | StoreStore | StoreExit | ||||
Volatile Load | LoadLoad | LoadStore | LoadLoad | LoadStore | LoadEnter | LoadExit |
Volatile Store | StoreLoad | StoreStore | StoreEnter | StoreExit | ||
MonitorEnter | EnterLoad | EnterStore | EnterLoad | EnterStore | EnterEnter | EnterExit |
MonitorExit | ExitLoad | ExitStore | ExitEnter | ExitExit |
另外,特殊的final字段规则需要一个StoreLoad屏障。
在这张表里,”Enter”与”Load”相同,”Exit”与”Store”相同,除非被原子指令的使用和特性覆盖。特别是: EnterLoad 在进入任何需要执行Load指令的同步块/方法时都需要。这与LoadLoad相同,除非在MonitorEnter时候使用了原子指令并且它本身提供一个至少有LoadLoad属性的屏障,如果是这种情况,相当于没有操作。 StoreExit在退出任何执行store指令的同步方法块时候都需要。这与StoreStore一致,除非MonitorExit使用原子操作,并且提供了一个至少有StoreStore属性的屏障,如果是这种情况,相当于没有操作。 ExitEnter和StoreLoad一样,除非MonitorExit使用了原子指令,并且/或者MonitorEnter至少提供一种屏障,该屏障具有StoreLoad的属性,如果是这种情况,相当于没有操作。
下面这个例子说明如何使用这些指令类型:
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
}
}
通过多处理器所支持的屏障和原子操作
Processor | LoadStore | LoadLoad | StoreStore | StoreLoad | Datadependencyorders loads? | AtomicConditional | OtherAtomics | Atomicsprovidebarrier? |
---|---|---|---|---|---|---|---|---|
sparc-TSO | 不执行操作 | 不执行操作 | 不执行操作 | membar(StoreLoad) | 是 | CAS:casa | swap,ldstub | 全部 |
x86 | 不执行操作 | 不执行操作 | 不执行操作 | mfence orcpuid orlocked insn | 是 | CAS:cmpxchg | xchg,locked insn | 全部 |
ia64 | combine和st.rel 或者ld.acq | ld.acq | st.rel | mf | 是 | CAS:cmpxchg | xchg,fetchadd | 部分 +acq/rel |
arm | dmb | dmb | dmb-st | dmb | 只能间接 | LL/SC:ldrex/strex | 仅针对部分 | |
ppc | lwsync | lwsync | l wsync | hwsync | 只能间接 | LL/SC:ldarx/stwcx | 仅针对部分 | |
alpha | mb | mb | wmb | mb | 否 | LL/SC:ldx_l/stx_c | 仅针对部分 | |
pa-risc | 不执行操作 | 不执行操作 | 不执行操作 | 不执行操作 | 是 | buildfromldcw | ldcw | 无 |
插入屏障(Inserting Barriers) 当程序执行时碰到了不同类型的存取,就需要屏障指令。编译器不知道指定的load或store指令是先于还是后于需要一个屏障操作的另一个load或store指令。最简单保守的策略是为任一给定的load,store,lock或unlock生成代码时,都假设该类型的存取需要“最重量级”的屏障:
总结:这些屏障中的有一些通常会简化成空操作。实际上,大部分都会简化成空操作,只不过在不同的处理器和锁模式下使用了不同的方式。最简单的例子,在x86或sparc-TSO平台上使用CAS实现锁,仅相当于在volatile store后面放了一个StoreLoad屏障。
移除屏障(Removing Barriers)
上面的保守策略对有些程序来说也许还能接受。volatile的主要性能问题出在与store指令相关的StoreLoad屏障上。这些应当是相对罕见的 —— 将volatile主要用于避免并发程序里读操作中锁的使用,仅当读操作大大超过写操作才会有问题。但是至少能在以下几个方面改进这种策略:
Origina | => | Transformed | ||||
---|---|---|---|---|---|---|
1st | ops | 2nd | => | 1st | ops | 2nd |
LoadLoad | [no loads] | LoadLoad | => | [no loads] | LoadLoad | |
LoadLoad | [no loads] | StoreLoad | => | [no loads] | StoreLoad | |
StoreStore | [no stores] | StoreStore | => | [no stores] | StoreStore | |
StoreStore | [no stores] | StoreLoad | => | [no stores] | StoreLoad | |
StoreLoad | [no loads] | LoadLoad | => | StoreLoad | [no loads] | |
StoreLoad | [no stores] | StoreStore | => | StoreLoad | [no stores] | |
StoreLoad | [no volatile loads] | StoreLoad | => | [no volatile loads] | StoreLoad |
重排代码(在允许的范围内)以更进一步移除LoadLoad和LoadStore屏障,这些屏障因处理器维持着数据依赖顺序而不再需要。 移动指令流中屏障的位置以提高调度(scheduling)效率,只要在该屏障被需要的时间内最终仍会在某处执行即可。 移除那些没有多线程依赖而不需要的屏障;例如,某个volatile变量被证实只会对单个线程可见。而且,如果能证明线程仅能对某些特定字段执行store指令或仅能执行load指令,则可以移除这里面使用的屏障。但是所有这些通常都需要作大量的分析。
杂记(Miscellany)
JSR-133也讨论了在更为特殊的情况下可能需要屏障的其它几个问题:
内存模型
Java内存模型基础
在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体)。通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。 在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。 同步是指程序用于控制不同线程之间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。 Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。 下面通过示意图来说明这两个步骤:
如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
Java内存模型的抽象 在java中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享(本文使用“共享变量”这个术语代指实例域,静态域和数组元素)。局部变量(Local variables),方法定义参数(java语言规范称之为formal method parameters)和异常处理器参数(exception handler parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。
Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:
如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。 从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
重排序 在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:
上述的1属于编译器重排序,2和3属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel称之为memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。
为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM把内存屏障指令分为下列四类: