JVM 基础 - JVM 内存模型
文章目录
- JVM 基础 - JVM 内存模型
- 前言
- 一、Java 内存模型(Java Memory Model)
- 1、Java堆栈
- 2、JMM概述
- 3、重排序问题
- 4、volatile关键字
- 防重排序
- 实现可见性
- 保证单次的读/写操作具有原子性
- 问题: i++为什么不能保证原子性?
- 5、先行发生规则(happens-before)
前言
很多人总是将Java内存模型和Java内存结构搞混,本文将详细讲解在Java中,什么是Java内存模型(Java Memory Model)。如果你需要了解Java内存结构的相关知识,请看另一篇文章: 《JVM 基础 - JVM 内存结构详解》。
一、Java 内存模型(Java Memory Model)
1、Java堆栈
JVM内部使用的Java内存模型在线程栈和堆之间划分内存。
2、JMM概述
Java 内存模型的主要目标是定义程序中各个共享变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。
Java 内存模型中规定了所有的变量都存储在主内存中,每个线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的主内存中的变量拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递(通信)均需要在主内存中来完成。
Java 内存模型的抽象示意图如下:
3、重排序问题
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:
1、编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2、指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3、内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
比如:A a = new A() 实际上是通过三个步骤完成,1:开辟一片内存空间,2:在内存空间初始化对象数据,3:将内存空间的地址赋值给对应的引用。由于重排序,可能实际执行的时候会变成:1:开辟一片内存空间,2:将内存空间的地址赋值给对应的引用,3:在内存空间初始化对象数据。如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量。
4、volatile关键字
防重排序
volatile的一个主要作用是防止重排序,也就是说,使用volatile关键字修饰的变量,在实际运行过程中,不会出现上述所说的重排,保证了程序的顺序执行。
实现可见性
volatile的另一个作用是实现变量的可见性,通俗的讲,就是一个线程修改了变量的值,另外的线程能够立马知道。
保证单次的读/写操作具有原子性
volatile不能保证完全的原子性,只能保证单次的读/写操作具有原子性。
问题: i++为什么不能保证原子性?
因为i++本质上是读、写两次操作,它是一个复合操作,包括三步骤:
1、读取i的值
2、对i加1
3、将i的值写回内存
运行如下代码:
public class VolatileTest {
volatile int i;
public void add(){
i++;
}
public static void main(String[] args) throws InterruptedException {
final VolatileTest test = new VolatileTest();
for (int n = 0; n < 1000; n++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.add();
}
}).start();
}
Thread.sleep(10000);//等待10秒,保证上面程序执行完成
System.out.println(test.i);
}
}
期望结果:i=1000,实际上i的值可能小于1000,证明volatile是无法保证 i++ 这三个操作的原子性的,可以通过AtomicInteger或者Synchronized来保证+1操作的原子性。
5、先行发生规则(happens-before)
JMM 为了保证有序,还内置了一套先行发生规则(happens-before)。两个操作间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行。happens-before 仅仅要求前一个操作对后一个操作可见,和一般意义上时间的先后是不一样的,达到逻辑上的顺序执行即可。
具体的规则如下:
1 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生与书写在后面的操作。【保证单线程的有序】
2 锁定规则:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
3 volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。【先写后读】
4 传递规则:A 先于 B 且 B 先于 C 则 A 先于 C
5 线程启动规则:Thread 对象的 start 方法先行发生于此线程的每一个动作。
6 线程中断规则:对线程 interrupt 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。【先中断,后检测】
7 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join() 方法结束,Thread.isAlive() 的返回值手段检测线程已经终止执行。
8 对象终结规则:一个对象的初始化完成先行发生于它的 finalize 方法的开始。
如果两个操作的执行顺序不能通过 happens-before 原则推导出来,就不能保证他们的执行次序,虚拟机就可以随意的对他们进行重排序。