JMM:Java内存模型。定义了主存(所有线程共享的数据)、工作内存(每个线程对应的私有数据)的抽象概念。
JMM存在以下几个特征
public class test{
static boolean run = true;
public static void main(String[] args){
new Thread(()->{
while(run){
//……
}
}).start();
System.out.println("主线程结束子线程");
run = false;
}
}
主线程结束子线程
理论上来讲,当run改为false后,子线程也会跟着结束并结束程序运行。但是实际并不会结束程序,因此主线程结束并不会让子线程结束while循环。
这就是可见性一个体现,不受CPU缓存影响。从JMM解释来看。
解决方案
将run变量使用volatile(异变)关键词修饰。这样就不会从缓存区获取run值,而是从主存中获取。
volatile关键词用来修饰成员变量与静态成员变量,修饰局部变量没意义,因为局部变量是线程私有的,主存中都没带存的。
或是使用synchroized加锁后来修改run的值。因为synchroized在进入保护的代码前会废弃工作内存重新再去主存中读取。[拓展:synchronized在获取锁之前需要去主存中获取保护代码块所需要的变量存储在工作内存中,当释放锁时会将工作内存中的变量刷新到主存中]。
volatile只能解决可见性问题,并不能解决指令交错的问题,因此只适用于一个线程写多个线程读的场景,比如说两个线程分别进行i++与i--,并不能保证能够正常得出结果。
synchronized虽然可以解决原子性与可见性的问题但是属于重量级操作,性能比较低。
JVM在不影响程序运行的正确性的前提下,会进行指令重排。在多线程下可能会存在安全隐患。
一般情况下赋值操作,是不在乎谁先谁后,因此可以进行指令重排的。但是在多线程下,有时需要使用这些变量,那么可能会存在安全隐患。
public class demo6 {
static int num;
static boolean ready;
static int result;
public static void main(String[] args) {
//线程1
new Thread(() -> {
if (ready) {
result = num + num;
} else {
result = 1;
}
}).start();
new Thread(() -> {
num = 2;
ready = true;
}).start();
}
}
对以上代码进行分析,原则上num与ready的赋值操作先后顺序是无所谓的。但是此时还存在线程1使用这两个变量,这两个变量的结果会对result的结果产生影响。
以上是指令重排序带来的危害,无法预测程序的运行结果,禁止指令重排只需要对ready变量使用volatile修饰即可。
写屏障:在volatile修饰的变量之前包括volatile变量,对于共享变量的变动会同步到主存。
读屏障:对于volatile变量之后的变量读取需要从主存中读取。
写屏障:在指令重排序时,不会将写屏障之前的代码排序到写屏障之后。
读屏障:在指令重排序时,不会将读屏障之后的代码排序到读屏障之前。
写屏障只能保证能够读取到最新的数据,并不能解决指令交错的问题。有序性只能保重本线程中的代码不会被指令排序
通过查看创建对象部分的字节码文件来看
前两行是获取单例对象并进行非空判断的字节码,如果不为空则跳转37处。这是第一层if判断
从6到36部分,是synchronized部分字节码,意思是获取类对象,复制一份存储在字符常量池中加锁,获取单例对象判断是否为空,为空则创建出一个对象,复制一份地址,根据地址调用构造器(21)后对单例对象进行复制后解锁,如果不为空跳转到37处。
问题在于21与24可能通过指令重排序后互换位置。那么在多线程中可能出现以下问题
线程2拿到了没有初始化的值去使用,造成空指针异常。