参考文献JMM概述-CSDN博客
内存模型可以理解为在特定的操作协议下,对特定的内存或者高速缓存进行读写访问的过程抽象描述,不同架构下的物理机拥有不一样的内存模型,Java虚拟机是一个实现了跨平台的虚拟系统,因此它也有自己的内存模型,即Java内存模型(Java Memory Model, JMM)。
因此它不是对物理内存的规范,而是在虚拟机基础上进行的规范从而实现平台一致性,以达到Java程序能够“一次编写,到处运行”。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是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(Java 内存模型) - 知乎 (zhihu.com)
JJM本质上是约定了线程之间或线程内部数据交换的规则,在JJM约定下,我们将内存空间分为两个部分,主内存与本地内存。注意:本地内存是一个抽象的概念,其实际并不存在,它包含了缓存,写缓冲区,寄存器以及其他硬件和编译器的优化
主内存:
JMM规定了所有的变量都存储在主内存中(此处的主内存与物理硬件的主内存名字一样,两者也可以“类比”,但物理上它仅是虚拟机内存的一部分)
本地内存(工作内存):
每条线程都有自己的工作内存(Working Memory,可以和处理器高速缓存“类比”),线程的工作内存中保存了该线程使用的变量的主内存副本。
内存数据的8种操作:
主内存中的操作:
工作内存中的操作:
JMM 还规定了上述 8 种基本操作,需要满足以下规则:
--------------------------以上来自参考博客,讲的非常详细,同时因为最近读了图解操作系统,刚好读到内存管理部分,因此对这一块的理解非常快,如果读者认为这一块比较难懂,不妨回头先研究操作系统对于内存管理的部分,再回顾以下内存的基本原理,回头再看这一块,理解起来就会很快了-------------------------------
但是在普通编程中,我们可能会出现以下示例的情况
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是如何解决这类问题的