链接:https://blog.csdn.net/weixin_44841312/article/details/120911816
一,什么是JMM?
它是一种java内存中数据存储的的协议规则:规定了内存是所有线程共有的,每个线程都有自己的工作内存,当进程需要使用数据时会去内存中读取数据放入自己的工作内存,然后对工作内存的数据进行操作,最终将操作后的数据写回到内存中。
二,八大操作
其实就是读写数据的原子操作,保证数据的读写不出现问题。
1,read操作:将数据从堆栈中读出来到一个缓冲区A
2,load操作:将缓冲区A的数据写入到工作内存
3,use操作:线程读取工作内存的数据进行计算
4,assign操作:线程在CPU中执行,将计算好的值存储到工作内存中
5,store操作:将工作内存大数据写入缓冲区B
6,write操作:将缓冲区B的数据写入原有的为止,对其进行赋值操作
7,lock操作:将堆栈中的变量加锁,标识为内存独占状态
8,unlock操作:堆栈中的变量解锁,其它线程就可以锁定该变量
三,volatile
保证可见性(线程A修改的数据对线程B来说是可见的)
不保证原子性(例如a++实现的a+1操作其实是有多个步骤组成,他们是可分割的),synchronize保证代码操作原子性
保证有序性(代码的执行过程中,为了提高执行的效率会发生执行的重新排列)
1,可见性验证:
private static int flag = 0; public static void main(String[] args) { //主线程写一个死循环 new Thread(()-> { while (flag==0) { System.out.println("a"); } flag = 1; }).start(); //终止死循环 flag=1; System.out.println(flag); //结果:无法终止死循环 //解决,使用volatile:private static volatile int flag = 0; //说明volatile可以保证程序的可见性 }
2,验证volatile不保证原子性
private volatile static int sum = 0; private volatile static AtomicInteger sum1 = new AtomicInteger(0); public static void main(String[] args) { //理论上的值为50000,但是由于volatile不能保证原子性,所以达不到50000 for (int i=0;i<50;i++) { new Thread(()-> { for (int j=0;j<1000;j++) { //其实它在底层的步骤为:获取值,加1,写回值 sum++; sum1.getAndIncrement(); } }).start(); } //保存计算的线程都执行完毕,留下的线程为main和gc while (Thread.activeCount()>2) { Thread.yield(); } System.out.println(sum); System.out.println(sum1); }
3,保证有序性,防止指令重排
什么是指令重排:我们写的程序真正执行是并不一定会按照我们认为的顺序执行的,为了提高程序的执行效率,
编译器优化时会发生重排,执行指令并行时会发生重排,内存系统也会发生重排
编译器在执行指令重拍时,会考虑数据间的依赖性
int y =1 //1
int y=2 //2
x=x+5 //3
y=x*x //4
我们期望执行的顺序:1234,最终执行的顺序可能是2134 1324,但不可能是4123,这由数据间的依赖性保证的
但是有时的指令重排是会导致数据出错的,此时如果加上了volatile关键字,就会在执行相应的命令组前后加上一组内存屏障,
在内存屏障中的指令就不会发生指令重排
内存屏障在单例模式中用的最多,单例模式使用枚举的方式创建是最适合的
public enum Singleton { INSTANCE;//该枚举类对象的实例 public void doSomething() { System.out.println("该枚举类实现的方法"); } } //调用的方式 public class Main { public static void main(String[] args) { Singleton.INSTANCE.doSomething(); } }