【内存模型】基础篇

 2019-12-10 16:07  阅读(849)
文章分类:Java Core

并发编程分类

在并发编程中,有两个问题需要解决。第一,线程之间如何通信。第二,线程之间如何同步。通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间通信机制:共享内存和消息传递。

共享内存并发模型:线程之间共享程序的公共状态,线程之间通过读-写内存中的公共状态来隐式进行通信。

消息传递并发模型:线程之间没有公共状态,线程之间必须通过明确的发送消息显示通信。

同步:控制不同线程之间操作发送相对顺序的机制。在共享内存并发模型里,同步是显示进行的。程序员必须显示指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接受之前,因此同步是隐式进行的。
Java并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

Java内存模型抽象

在Java中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享。局部变量、方法定义参数和异常处理参数不会在线程之间共享。

Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。抽象来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程有自己的私有的本地内存。本地内存中存储了该线程读/写共享变量的副本。本地内存是一个抽象概念,并不真实存在。

20191210001575\_1.png20191210001575\_2.png

上述图中,线程A和线程B通信,必须经过以下两步骤:1)线程A把本地内存中共享变量刷新到主内存中。2)线程B到主内存中读取线程A已经更新过的共享变量。从整体来看,这两个步骤实质上是线程A向线程B发送消息,而且此通信过程必须通过主存。JMM通过控制主内存与每个线程之间的本地内存的交互,为程序员提供内存可见性的保护。

重排序

在执行程序时,为了提高性能,编译器和处理器通常会对指令做重排序,主要分三类:

1)编译器优化的排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

2)指令级并行的重排序。现代处理器采用指令级并行技术来将多条指令重叠执行。若不存在数据依赖,处理器可以改变语句对应的机器指令的执行顺序。

3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,使得加载和存储操作看上去可能是乱序执行。

从Java源码到最终实际执行的指令序列,会分别经历以下三种重排序:

Java源代码   --->  编译器优化排序 ---> 指令级并行冲排序 ---> 内存系统重排序 ---> 最终执行的指令序列

上述重排序都会导致多线程程序出现内存可见性问题。对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers或memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。

为了保证内存可见性,Java编译器会在生成指令序列的合适地方插入内存屏障指令来禁止特定类型处理器重排序。JMM把内存屏障指令分为以下四类:

20191210001575\_3.png

storeload Barriers是一个”全能型”屏障,它同时具备其他三个屏障的效果。执行该屏障开销很昂贵,因为当前处理器通常把写缓冲区中的数据全部刷新到内存。

happens-before

Java内存模型使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,若一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里的两个操作可以在一个线程之内,也可以在不同线程之间。

其规则如下:

1)程序顺序规则:一个线程的每个操作,happens-before于该线程中的任意后续操作。

2)监视器锁规则:对一个监视器的解锁,happens-before于随后对这个监视器的加锁。

3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 【内存模型】基础篇

相关推荐