目录
一、认识JMM
JMM就是Java内存模型(java memory model)。
JMM的作用(屏蔽不同操作系统、硬件生产商带来的差异)
因为在不同的硬件生产商和不同的操作系统下,内存访问有一定的差异,所以会造成相同的代码运行在不同的系统上面会产生各种问题。
所以Java内存模型屏蔽掉各种硬件和操作系统内存访问的差异,以实现让java程序在各种平台下面都可以达到一致的并发效果
——以上来源于《知乎》阿里云云栖号
JMM有哪些特殊的规定
主内存
Java内存模型规定:所有的变量都存储在主内存(Main Memory)当中:例如:
一个类的实例变量、静态变量
但是不包括局部变量和方法的参数。
线程的工作内存
每一个线程都有自己的工作内存。每一个线程不可以直接读取主内存当中的变量的值。
如果想要读取,那么就一定需要把主内存当中的变量临时拷贝一份到线程的工作内存当中。
不同的线程的工作内存不可以直接交互,必须要通过主内存来进行交互。
工作内存与主内存交互的8种方式
以下的8种方式,都是原子性的。
作用于主内存的3种方式
方式1:lock(锁定)
作用于主内存的变量、它把一个变量标识为一个线程独占的状态。
方式2:unlock(解锁)
作用于主内存的变量,把一个处于锁定状态的变量给释放出来,于lock互为逆向的操作。
方式3:read(读取)
作用于主内存的变量,它把一个变量的值从主内存传输到工作内存当中,以便以后的load。
方式4:write(写入)
把store到的值传入到主内存当中。
作用于线程工作内存变量的4种方式
方式5:load(载入)
作用于线程的工作内存当中的变量:它把read操作从主内存当中得到的值放入线程的工作内存当中。
方式6:use(使用)
作用于工作内存当中的变量,它把一个工作内存当中一个变量的值传递给执行引擎。
方式7:assign(赋值)
与6是互逆操作,把执行引擎当中的值传递给线程工作内存当中的变量。
方式8:store(存储)
把线程的工作内存当中的变量传递给主内存当中。
二、了解内存可见性:
线程针对变量的修改,可以及时被其他线程所获取到。
在上一篇文章:
对于synchronized的初步认识当中,已经提到了什么是内存可见性问题:
给出一个业务场景:
此时有两个线程,其中一个线程尝试通过Scanner获取输入的值,把输入的值赋值给count,另外一个线程尝试读取count的值,此时读取到的值,不一定是修改之后的值
public static void main(String[] args) {
MyCounter myCounter=new MyCounter();
//t1循环去读取
// thread2尝试去修改count的值
Thread thread2=new Thread(()-> {
Scanner input=new Scanner(System.in);
System.out.println("请输入一个整数:");
myCounter.count=input.nextInt();
});
Thread thread1=new Thread(()-> {
while (myCounter.count==0){
System.out.println(myCounter.count);
}
System.out.println("循环结束...!");
});
thread1.start();
thread2.start();
}
观察代码的运行结果,thread2即使输入了一个非0的数,thread1也没有立刻输出"循环结束"这一句话。
thread1仍然在不断地循环当中。似乎没有检测到thread2对于count的修改。
产生这种现象的本质原因,是编译器优化:
在thread1的循环当中,循环的条件是:myCounter.count==0,如果把这句条件判断放在汇编语言的角度,myCounter.count相当于在内存当中加载count的值,这个步骤把它称之为load;判断count==0这个步骤相当于cmp。其中前一个操作:load,这个耗时远大于cmp这个操作。于是编译器为了省时间,就不再进行新的读取操作了。图解:(内存不可见,箭头代表从内存当中读取)
如图所示:load这一项指令的耗时,与cmp相比,耗费了相当长的时间,并且在循环语句当中,每秒可以执行相当多的次数,如果再把load的操作反复执行,那就会延长程序运行的时间。
于是编译器下次再load的时候,就不再从内存当中继续读取数据了,而是从自己的工作内存当中读取,默认此时没有线程2:Thread2针对count进行修改。
三、使用volatile关键字解决内存可见性问题:
class MyCounter{
volatile public int count=0;
}
两个作用:
①当写一个volatile变量的时候,JMM会把该线程工作内存当中的值同步修改到主内存当;
②当线程读取一个volatile变量的时候,JMM会把所有线程的工作内存当中的这个变量置为无效,从而让线程去主内存当中获取到最新的变量的值。
但是需要注意的是,虽然volatile可以保证内存的可见性,但是仍然无法保证原子性。即:(把变量从内存加载出来,修改变量的值为1,再把变量存储到内存当中)这三个操作。
四、指令重排序(保证有序性)
何为指令重排序?
站在编译器的角度,可能针对真实程序代码背后的汇编代码指令,进行重新排序,来达到提升执行效率的效果。但是使用了volatile关键字之后,可以保证避免指令重新排序的问题。
需要注意的是,虽然synchronized也保证了有序性,但是synchronized保证有序性的原理是实现了互斥使用,让synchronized内部的代码块同一时刻是单线程的,无法保证指令重排序。
而volatile实现有序性的原理是禁止指令重排序。
,