Java 内存模型(JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量的访问方式,包括实例字段、静态字段和构成数组对象的元素。
Java 内存模型(JMM):
JMM 中的主内存:存储 Java 实例对象,包括成员变量、类信息、常量、静态变量等,属于数据共享的区域,多线程并发操作会引发线程安全问题。
JMM 中的工作内存:存储当前方法的所有本地变量信息,本地变量对其他线程不可见 。还存储了字节码行号指示器、Native 方法信息。属于线程私有数据区域,不存在线程安全问题。
主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新回主内存。
JMM 与 Java 内存区域(见上图运行时数据区)的划分是不同的概念层次,JMM 描述的是一组规则,围绕原子性、有序性、可见性展开。
在执行程序的时候,为了提高性能,处理器和编译器常常会对指令进行重排序,指令重排序需要满足以下两个条件:
也就是无法通过 happens-before 原则推导出来的,才能进行指令的重排序。
JVM 内部的实现通常依赖于所谓的内存屏障,通过禁止某些重排序的方式提供内存可见性保证。A 操作的结果需要对 B 操作可见,则 A 与 B 必须存在 happens-before 关系。
happens-before 原则非常重要,它是判断数据是否存在竞争,线程是否安全的主要依据。
volatile 关键词是 JVM 提供的轻量级同步机制,保证了被 volatile 修饰的共享变量对所有线程总是可见的,并且禁止指令重排序优化。
volatile 关键词强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。
private static volatile int value = 0;
线程主体(工作内存) <—— volatile 变量写入/读取——> 主内存
使用 volatile 关键词增加了实例变量在多个线程之间的可见性。但 volatile 关键词最致命的缺点是不支持原子性。volatile 关键词虽然增加了实例变量在多个线程之间的可见性,但它却不具备同步性,那么也就不具备原子性。例如以下代码:
public class VolatileVisibility {
private static volatile int value = 0;
public static void increase() {
value++;
}
}
value 值的任何改变,都会立马反应到线程当中,如果现在存在多条线程同时调用 increase() 方法,就会出现线程安全问题,因为 value++; 这个操作并不具备原子性,value++; 是先读取值,再写回一个新值,分两步完成,如果另外一个线程在两步之间读取 value 值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同的 +1 操作,也就引发了线程安全的问题。
所以对于 increase() 方法必须使用 synchronized 修饰,保证线程安全:
public class VolatileVisibility {
private static volatile int value = 0;
public synchronized static void increase() {
value++;
}
}
由于 synchronized 会创建一个内存屏障,保证所有结果都刷新到主存中去,保证了操作的内存可见性,所以 volatile 也就可以省略了:
public class VolatileVisibility {
private static int value = 0;
public synchronized static void increase() {
value++;
}
}
synchronized 与 volatile 对比:
关键词 | 性能 | 修饰范围 | 多线程访问 | 原子性可见性 | 指令重排序 | 主要解决问题 |
---|---|---|---|---|---|---|
synchronized | 可修饰方法、代码块 | 会阻塞 | 可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据同步 | 可以被重排序优化 | 解决的是多个线程之间访问资源的同步性 | |
volatile | 轻量级实现,性能要好 | 可修饰变量 | 不会阻塞 | 可以保证变量的修改可见性,不能保证原子性 | 禁止重排序 | 解决的是变量在多个线程之间的可见性 |
线程安全包含原子性和可见性两个方面,Java 的同步机制都是围绕这两个方面来确保线程安全的。
volatile 的本质是告诉 JVM 当前变量在工作内存中的值是不确定的,需要从主存中读取。
内存屏障是一个 CPU 指令,其作用有两个:
volatile 通过插入内存屏障指令,禁止在内存屏障前后的指令执行重排序优化。
volatile 强制刷出各种 CPU 的缓存数据,因此任何 CPU 上的线程都能读取到这些数据的最新版本。