一.什么是JMM
JMM就是Java内存模型但它不是指具体的内存区域而是一种虚拟机规范,JMM规范屏蔽各个硬件平台和操作系统对内存访问机制的差异化。为什么要提出这个规范呢?因为java线程(用户线程)是映射到内核线程的,当用户线程需要申请系统资源(内存,IO等这些资源是受操作系统保护用户线程不能直接操作)是需要切换到内核线程让内核线程去操作,例如java线程定义变量时为变量申请内存的操作就是需要切换到内核线程,这种情况下对内存的访问就需要有一种协同机制,并且内存还会存在缓存一致性问题,为了解决这些问题就提出了JMM。所以JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。 JMM描述的是一种抽象的概念,一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性、有序性、可见性展开的。
JMM确保了多线程环境下的数据一致性和可见性,主要通过解决原子性、有序性、可见性三个关键问题来实现这一点。
- 原子性:JMM确保了操作在多线程环境中的原子性,即操作要么完全执行,要么完全不执行,不会出现部分执行的情况。这有助于确保在多线程环境中对共享资源的正确访问。
- 可见性:JMM解决了多线程间的可见性问题,确保当一个线程修改了共享变量的值后,其他线程能够立即看到这个变化。这是通过规定线程对共享变量的所有操作都必须在自己的工作内存中进行,并且不同线程之间无法直接访问其他线程工作内存中的变量,变量值的传递需要通过主内存来完成来实现的。
- 有序性:JMM通过规定指令的重排序规则来保证程序执行的有序性。指令重排序可能会影响并发程序的正确性,因此JMM通过一定的规则来限制指令的重排序,以确保程序的执行结果符合预期。
二.JMM内存模型
每个线程访问共享变量时,会从主存复制变量的副本到线程本地内存
本地内存刷新的时机有:
1. 线程启动时会根据主存初始化本地内存
2. 进入 synchronized
代码块
3.读取volatile关键字修饰的变量
4.使用Unsafe相关方法(
跟cas有关同理使用ReentrantLock等也会有同等效果
)
5.执行Thread.sleep,
Thread.yield后,这个我从代码验证确实会出现可见性问题(后面有代码)但是原理就很抽象,看下GPT的回答:
本地内存同步到主存的时机:
1.退出synchronized
代码块
2.修改volatile修饰的变量
3.使用Unsafe修改变量
4.线程的调度在某些情况下
5. 如果一个线程在没有使用 synchronized
、volatile
或其他显式同步机制的情况下对变量进行修改,那么这个修改刷新到主存的时机是不确定的,这取决于多种因素,包括编译器优化、JVM 实现、操作系统和硬件的调度等,这种情况下,线程间的共享变量的可见性和一致性并不受保证。
三.验证JMM可见性
原子性,顺序性相对来说容易理解写,可见性就看不见摸不着,下面只列一些关于可见性的验证代码:
public class ThreadNotice {
private static boolean run = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (run){
}
System.out.println("t1:"+run);
},"t1").start();
//主线程等待一秒保证t1线程先执行
Thread.sleep(1000);
run = false;
System.out.println("mian:"+run);
}
}
代码主要是为了让t1线程先执行进入无限循环,然后主线程修改run=false,看看t1线程会不会对run的修改可见并退出循环,看下执行情况:
主线程修改run为false,当主线程修改完run,程序没有退出,说明t1还在执行并没有感知到run已被修改为false,你要是不相信t1还在运行,咱就有图有那啥:
为什么不把System.out.println("t1:"+run);放在while的里面看一下每次执行run的值?这样是不行的,这样t1就会感知run的变化退出循环,看下System.out.println的源码
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
加了synchronized,当一个线程获取锁时它会清空或刷新该线程的本地内存,并从主内存中重新加载变量的值。这也是JMM解决原子性、有序性、可见性的机制之一,另外使用volatile
关键字也能保证有序性,可见性, 改下代码再运行下:
给run 加上volatile
可以看到主线程修改run=false后,t1线上就会感知run的变化跳出循环
使用Thread.sleep,
Thread.yield的情况:
总结:先这样吧,后面有新发现再补充