了解JAVA内存模型(JMM)
1、概述
我们常说的JMM指的是Java内存模型(Java Memory Model,JMM),主要用于控制Java程序解决线程间如何通信和数据同步,JMM规范了多线程访问共享内存时的 可见性、有序性和原子性。
- 所有的共享变量都存在主内存中;
- 每个线程都保存了一份该线程使用到的共享变量的副本。
- 如果线程A与线程B之间要通信的话,必须经历下面2个步骤:
线程A将本地内存A中更新过的共享变量刷新到主内存中去。
线程B到主内存中去读取线程A之前已经更新过的共享变量。
因此,线程A无法直接访问线程B的工作内存,那是因为工作内存是线程独有的,线程间通信必须借助主内存,这也是JMM中的规定。当主内存中的共享变量被某个线程更新时,JMM会通过控制主内存与每个线程的本地内存之间的交互,来提供内存可见性保证。因此通过JMM规范,有效的解决了以下问题:
-
可见性问题:JMM保证对于一个线程对变量的修改,其他线程能够立即看到这个修改,从而避免了线程之间读取数据不一致的问题;
-
有序性问题:JMM保证程序的执行顺序是有序的,即按照代码的编写顺序执行,从而避免了出现代码执行顺序混乱的问题;
-
原子性问题:JMM保证对单个变量的读取和写入操作是原子性的,即不会出现数据竞争问题。
2、JMM内存模型的实现
2.1、简介
Java内存模型规范了JVM如何按需禁用缓存和编译优化,具体包括volatile
、synchronized
、final
这几个关键字,以及Happens-Before
规则。
2.2、原子性
原子性指的是指一个操作是不可中断的,即多线程环境下,操作不能被其他线程干扰。在Java中,最常用的便是使用关键字synchronized
进行原子性的保证。
2.3、可见性
一个未声明
volatile
的变量,都是从各自的cpu
缓存获取数据,线程更新数据之后,其他线程无法获取最新的值。而使用volatile
声明的变量,表明禁用缓存,更新数据直接更新到内存中,每次获取数据都是直接内存获取最新的数据。线程之间的数据都是相互可见的。
可见性来自happens-before
规则,happens-before
用来描述两个操作的内存可见性,如操作Ahappens-before
操作B,那么A的结果对于B是可见的,前面的一个操作结果对后续操作是可见的。happens-before
定义了以下几个规则:
- 解锁操作
happens-before
同一把锁的加锁操作。 - volatile 字段的写操作
happens-before
同一字段的读操作。 - 线程的启动操作
happens-before
该线程的第一个操作。 - A
happens-before
B,且Bhappens-before
C,那么Ahappens-before
C。happens-before
具有传递性。
2.4、有序性
指程序是有序的按照一定的顺序运行,这一特性主要是针对于操作系统中对程序指令进行重排序造成的并发乱序问题。为了性能和便捷,在JMM中指明,再不改变程序执行结果的前提下,允许编译器和处理器对程序优化进行重排序。
在Java中,可以使用synchronized
和volatile
来保证多线程之间操作的有序性。实现方式有所区别:
volatile
关键字会禁止指令重排;synchronized
关键字保证同一时刻只允许一条线程操作。
如果代码没有依赖关系,JVM
编译优化可以对他们随意的重排序
,比如method1
方法没有依赖关系,进行重排序:
int a=0, b=0;
public void method1() {
int r2 = a;
b = 1;
}
public void method2() {
int r1 = b;
a = 2;
}
此时在多线程环境下,两个线程交替运行method1
和method2
方法:重排序后r1
、r2
分别是0
,0
。
那如何解决重排序的问题呢?答案就是将变量声明为volatile
,比如a
或者b
变量声明volatile
。比如b
声明为volatile
,此时b
的赋值操作要happens-before
r1
的赋值操作。
int a=0;
volatile int b=0;
public void method1() {
int r2 = a;
b = 1;
}
public void method2() {
int r1 = b;
a = 2;
}
同一个线程顺序也满足happens-before
关系以及传递性,可以得到r2
的赋值happens-before
a
的赋值。也就表明对a
赋值时,r2
已经完成赋值了。也就不可能出现r1
、r2
为0
、0
的结果。
总结
Java
内存模型(Java Memory Model,JMM)定义了Java
程序中多线程之间共享变量的访问规则,以及线程之间的交互行为。它规定了线程如何与主内存和工作内存交互,以确保多线程程序的可见性、有序性和一致性。
- 可见性:使用
volatile
声明变量,数据读取直接从内存中读取,更新也是强制刷新缓存,并同步到主内存中。 - 有序性:使用
volatile
声明变量,确保编译优化不会重排序该字段。 - Happens-Before: 前面一个操作的结果对后续操作是可见的,