JMM
什么是JMM (Java Memory Model)
参考文献JMM概述-CSDN博客
内存模型可以理解为在特定的操作协议下,对特定的内存或者高速缓存进行读写访问的过程抽象描述,不同架构下的物理机拥有不一样的内存模型,Java虚拟机是一个实现了跨平台的虚拟系统,因此它也有自己的内存模型,即Java内存模型(Java Memory Model, JMM)。
因此它不是对物理内存的规范,而是在虚拟机基础上进行的规范从而实现平台一致性,以达到Java程序能够“一次编写,到处运行”。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。本地内存它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化之后的一个数据存放位置
JMM的三大特点
参考文献JMM概述-CSDN博客
原子性
一个操作不能被打断,要么全部执行完毕,要么不执行。如同事务一样
可见性
一个线程对共享变量做了修改之后,应该通知其他线程,使其他线程能够立即看到共享变量被修改
Java内存模型是通过将在工作内存中的变量修改后的值同步到主内存,在读取变量前从主内存刷新最新值到工作内存中,这种依赖主内存的方式来实现可见性的。
无论是普通变量还是volatile变量都是如此,区别在于:volatile的特殊规则保证了volatile变量值修改后的新值立刻同步到主内存,每次使用volatile变量前立即从主内存中刷新,因此volatile保证了多线程之间的操作变量的可见性,而普通变量则不能保证这一点。
除了volatile关键字能实现可见性之外,还有synchronized,Lock,final也是可以的。
使用synchronized关键字,在同步方法/同步块开始时(Monitor Enter),使用共享变量时会从主内存中刷新变量值到工作内存中(即从主内存中读取最新值到线程私有的工作内存中),在同步方法/同步块结束时(Monitor Exit),会将工作内存中的变量值同步到主内存中去(即将线程私有的工作内存中的值写入到主内存进行同步)。
使用Lock接口的最常用的实现ReentrantLock(重入锁)来实现可见性:当我们在方法的开始位置执行lock.lock()方法,这和synchronized开始位置(Monitor Enter)有相同的语义,即使用共享变量时会从主内存中刷新变量值到工作内存中(即从主内存中读取最新值到线程私有的工作内存中),在方法的最后finally块里执行lock.unlock()方法,和synchronized结束位置(Monitor Exit)有相同的语义,即会将工作内存中的变量值同步到主内存中去(即将线程私有的工作内存中的值写入到主内存进行同步)。
final关键字的可见性是指:被final修饰的变量,在构造函数数一旦初始化完成,并且在构造函数中并没有把“this”的引用传递出去(“this”引用逃逸是很危险的,其他的线程很可能通过该引用访问到只“初始化一半”的对象),那么其他线程就可以看到final变量的值。
有序性
在单线程的情况下,代码总是串行地从前往后执行,但在多线程并发情况下,代码执行的顺序可能就会出现乱序。虽然在线程与线程之间,所有操作都是无序的,但我们能够保证:在线程内部所有操作都是有序的,因此,我们在编程过程中可以适当忽略掉无序性。但我们能够使用Java中的一些特性,使线程与线程之间的操作变得有序,如synchronized、volatile、lock等
JMM原理
参考博客:从线程三大特性深入理解JMM(Java 内存模型) - 知乎 (zhihu.com)
JJM本质上是约定了线程之间或线程内部数据交换的规则,在JJM约定下,我们将内存空间分为两个部分,主内存与本地内存。注意:本地内存是一个抽象的概念,其实际并不存在,它包含了缓存,写缓冲区,寄存器以及其他硬件和编译器的优化
主内存:
JMM规定了所有的变量都存储在主内存中(此处的主内存与物理硬件的主内存名字一样,两者也可以“类比”,但物理上它仅是虚拟机内存的一部分)
本地内存(工作内存):
每条线程都有自己的工作内存(Working Memory,可以和处理器高速缓存“类比”),线程的工作内存中保存了该线程使用的变量的主内存副本。
内存数据的8种操作:
-
主内存中的操作:
- **lock(锁定):***把一个变量标记为一条线程的独占状态。
- ***unlock(解锁):***把一个处于锁定状态的变量释放出来。
- ***read(读取):***它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 *load* 动作使用。
- ***write(写入):***它把 *store* 操作从工作内存中得到的变量值放入主内存的变量中。
-
工作内存中的操作:
- load(载入)😗 它把 *read* 操作从主内存中得到的值放入工作内存的变量副本中。
- ***use(使用):***它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- ***assign(赋值):***它把一个从执行引擎收到的赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- ***store(存储):***它把工作内存中一个变量的值传送到主内存中,以便后续的 *write* 操作使用。
JMM 还规定了上述 8 种基本操作,需要满足以下规则:
- 不允许 *read* 和 *load* 、*store* 和 *write* 操作单一出现。
- 不允许一个线程丢弃它最近的 *assign* 操作,及工作内存值被改变之后必须同步回主内存。
- 不允许一个线程无原因的(没有发生过任何 *assign* 操作)把数据从线程的工作内存同步回主内存
- 一个新变量只能从主存中”诞生“,也就是说 *user* 、*store* 操作之前,必须先执行 *assign* 和 *load* 操作。
- 一个变量在同时只能被一个线程 *lock* ,但可以被同一个线程多次 *lock* ,之后只有执行相同次数的 *unlock* 才能被解锁。
- 如果一个变量执行了 *lock* 操作,将会清空工作内存中的此变量的值,在执行引擎使用这个变量前,需要重新执行 *load* 或 *assign* 操作初始化变量值。
- 如果一个变量没有被 *lock* ,那就不允许对它执行 *unlock* 操作,也不允许去 *unlock* 一个被其他线程 *lock* 的变量。
- 对第一个变量执行 *unlock* 之前,必须把此变量同不回主内存中(执行 *store* 、 *write* 操作)。
--------------------------以上来自参考博客,讲的非常详细,同时因为最近读了图解操作系统,刚好读到内存管理部分,因此对这一块的理解非常快,如果读者认为这一块比较难懂,不妨回头先研究操作系统对于内存管理的部分,再回顾以下内存的基本原理,回头再看这一块,理解起来就会很快了-------------------------------
但是在普通编程中,我们可能会出现以下示例的情况
package Volatile;
import java.util.concurrent.TimeUnit;
public class JMMDemo {
private static int num = 0;
public static void main(String[] args) throws InterruptedException {// 主线程
new Thread(()->{// 线程1
while(num == 0){
}
}).start();
TimeUnit.SECONDS.sleep(1);
num = 1;
System.out.println(num);
}
}
/**
* 在本示例中,按照一贯的思想:
* 线程一将先运行,主线程睡眠1秒
* 当主线程睡眠时,因为num=0,因此线程一一直在跑
* 当主线程醒来时,会在抢到CPU时将num变量修改为1,此时num=1
* 主线程打印num变量,结果为1
* 按道理来讲,num=1,当线程一应当停止while循环并结束线程
* 当读者运行一下这段程序,会发现什么?
* 答:程序还在跑,并没有停止,也就是说,线程一根本没有停止!
*/
通过学习上面的JMM,我们可以回答为什么这个程序一直在跑的原因:当主线程修改num变量时,并没有通知线程一,也就是说,线程一不知道num已经被修改了。
这样的编程没有遵循JMM的约定,即:可见性(主线程并没有通知线程一变量已被改变)
因此,在并发编程中:如若一个线程要修改共享资源,必须要通知其它线程。
下一章节我们将看到Java是如何解决这类问题的