一.Java内存模型概述
Java内存模型即Java Memory Model,简称JMM。
Java内存模型描述了在多线程代码中哪些行为是合法的,以及线程如何通过内存进行交互。
它描述了“程序中的变量“ 和 ”从内存或者寄存器获取或存储它们的底层细节”之间的关系。Java内存模型通过使用各种各样的硬件和编译器的优化来正确实现以上事情。
Java包含了几个语言级别的关键字,包括:volatile, final以及synchronized,目的是为了帮助程序员向编译器描述一个程序的并发需求。Java内存模型定义了volatile
和synchronized
的行为,更重要的是保证了同步的java程序在所有的处理器架构下面都能正确的运行。
对于程序员来说,JMM是一套协议,这套协议描绘了一个模型,通过认知这个模型,按照协议的规定,程序员编写出的代码可以由JMM来保障(在多线程环境下)如期运行而不用在意各种硬件和操作系统的内存访问差异.
1.线程通信与同步
在并发编程领域,有两个关键问题:线程之间的通信
和同步
。
1).线程之间的通信
线程的通信是指线程之间以何种机制来交换信息
。
在命令式编程中,线程之间的通信机制有两种共享内存和消息传递。
共享内存并发模型的通信
- 定义
线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式
进行通信 - 典型的共享内存通信方式
通过共享对象进行通信。
- 定义
消息传递并发模型的通信
- 定义
线程之间没有公共状态,线程之间必须通过明确的发送消息来显式
进行通信 - 典型的消息传递方式
wait()和notify()。
- 定义
2).线程之间的同步
线程的同步是指程序用于控制不同线程之间操作发生相对顺序
的机制。
共享内存并发模型的同步
显式进行
:程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。消息传递并发模型的同步
隐式进行
:由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
并发模型 | 通信 | 同步 |
---|---|---|
共享内存 | 隐式 | 显式 |
消息传递 | 显式 | 隐式 |
Java的并发采用的是共享内存
模型
Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。
另外Java线程之间的同步需要程序员显式指定互斥执行.
2.变量与内存
1).变量分类(共享与不共享)
在java中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享(本文使用“共享变量”这个术语代指实例域,静态域和数组元素)。局部变量(Local variables),方法定义参数(java语言规范称之为formal method parameters)和异常处理器参数(exception handler parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。
- 共享(受内存模型影响):所有实例域、静态域和数组元素
- 不共享(不受内存模型影响):局部变量,方法参数,异常处理器参数
后文内存模型中说到的变量一般指共享变量.
2).主内存与本地内存(工作内存)
JMM决定一个线程对共享变量的写入何时对另一个线程可见。
线程和内存之间的关系:
线程之间所有的共享变量存储在主内存
(main memory)中,
每个线程都有一个私有
的本地内存
(local memory),本地内存中存储了该线程以读/写共享变量的副本。
本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。工作流程
- 创建本地内存:
JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(即本地内存) - 读取主内存:
变量从主内存拷贝到本地内存空间 - 操作本地内存:
对变量进行操作 - 回写主内存:
操作完成后再将变量写回主内存
- 创建本地内存:
- 注意:
- 主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行
- 在JMM语义下,主内存和本地内存存储的都是共享变量(原本和副本的区别)
非共享变量(如局部变量)的存储位置不在JMM语义下作讨论 - 有些人喜欢把主内存和本地内存跟JVM内存空间的java堆和java虚拟机栈作对比,但其实并不严谨,主内存和本地内存本来就是抽象概念,并不真实对应,所以两者基本上没有关系
不过理解上,有一部分人认为本地内存对应java虚拟机栈,程序计数器等线程私有的空间,另一部分人(如<深入理解java虚拟机>)则认为本地内存对应虚拟机栈的部分区域,我更偏向于后者的观点,因为主内存和本地内存本来就是JMM语义下的产物,其实现一定是线程私有的,故应当在java虚拟机栈中.但JMM语义外的非共享变量(如局部变量)虽然也是在虚拟机栈中,却不一定属于本地内存.
另外从更低层次说,主内存应该对应于物理硬件的内存,而本地内存(工作内存)则应该存储于寄存器和高速缓存中.
内存类型 | 访问权限 | 内容 | 功能 |
---|---|---|---|
主内存 | 线程共享 | 共享变量 | 储存所有的共享变量 |
本地内存(工作内存) | 线程私有 | 共享变量的副本 | 完成线程对变量的操作(读写) |
3).共享内存并发模型的通信
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
- 线程A把本地内存A中更新过的共享变量刷新到主内存中去。
- 线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图来说明这两个步骤:
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存(隐式通信)。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
二.Java内存模型的承诺
JMM能在多线程以及重排序的环境下给程序员承诺原子性,可见性和有序性,并通过Java内存模型的各种保障机制来实现
1.重排序
重排序是程序员需要Java内存模型给予承诺的重要原因
1).重排序分类
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型: