一:内存可见性
public class Demo1 {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
while(count==0){
}
System.out.println("t1线程结束");
});
Thread t2=new Thread(()->{
Scanner scanner=new Scanner(System.in);1
System.out.println("请输入一个整数:");
count=scanner.nextInt();
});
t1.start();
t2.start();
}
}
上述代码预期效果:
t1线程首先进入循环,当用户输入一个非0整数的时候,就会使t1线程退出循环.结束线程.
但t1实际上并没有真正出现退出的情况,这也是"bug",而产生上述现象的原因,就是**“内存可见性”**
while(count==0){
}
从指令角度分析这段代码:
(1)load :从内存读取数据到CPU 寄存器中,
(2)cmp:比较,条件成立,继续循环,条件不成立,退出循环.
然而,一个load指令消耗的时间,会比一个cmp指令消耗的时间多得多,执行一次load的时间,等于上万次cmp执行消耗的时间.
同时,JVM发现每次load执行的结果,是一样的(t2线程修改之前),因此,**JVM就把上述load操作优化掉了,只是第一次真正进行load,后续再执行到load,而是直接读取已经load过的寄存器中的值了(读取寄存器的速度远远大于 读取内存的速度).**当t2线程修改count的值,但由于t1线程并没有从内存中重新读取,所以获取不到更新后的值.
1.2:
public class Demo1 {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
while(count==0){
System.out.println("hello ");
}
System.out.println("t1线程结束");
});
Thread t2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("请输入一个整数:");
count=scanner.nextInt();
});
t1.start();
t2.start();
}
}
当我们在
while(count==0){
System.out.println("hello ");
}
while循环中打印,就会发现代码又和我们预期的效果一样了,这又是为什么???
因为循环体内存在IO操作,而IO操作是从硬盘中获取数据,因此IO操作消耗的时间比load操作消耗的时间更多,并且IO操作是不能被优化掉的.
总结:上述问题本质上是编译器优化引起的(优化是由javac和java配合完成的工作),优化掉load操作之后,使t2线程的修改,没有被t1线程感知到,这就造成了"内存可见性"问题
二:解决内存可见性问题
2.1 volatile关键字
编译器什么时候优化,什么时候不优化,这是一个"玄学问题".
通过volatile关键字,解决优化问题,让编译器不再优化.
当给变量修饰上volatile关键字之后,编译器就知道了,这个变量是"反复无常"的,编译器就不会再进行优化了
volatile 是专门针对内存可见性的场景来解决问题的,告诉编译器不要进行优化操作.
public class Demo1 {
public volatile static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
while(count==0){
}
System.out.println("t1线程结束");
});
Thread t2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("请输入一个整数:");
count=scanner.nextInt();
});
t1.start();
t2.start();
}
}
2.2:synchronized关键字解决内存可见性问题
synchronized关键字,和volatile关键字处理逻辑上是不同的.
引入synchronized关键字,是因为加锁操作本身太重量了,相比load 来说,开销更大,编译器自然就不会对load 优化了(和sleep ,IO操作原理类似).
public class Demo1 {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1=new Thread(()->{
while(count==0){
synchronized (locker){{
}}
}
System.out.println("t1线程结束");
});
Thread t2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("请输入一个整数:");
count=scanner.nextInt();
});
t1.start();
t2.start();
}
}