内存可见性:
先看一段代码:
public class TestDemo {
static class Counter {
public int counter = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(){
@Override
public void run() {
while(counter.counter == 0) {
}
System.out.println("循环结束");
}
};
Thread t2 = new Thread(){
@Override
public void run() {
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个整数");
counter.counter = sc.nextInt();
}
};
t1.start();
t2.start();
}
}
预期效果是:
线程1会先进入循环,线程2会读取一个用户输入的整数
随着用户输入了一个非0的整数之后,此时线程2读取到数字并修改counter的值
然后线程1发现counter已不满足条件,就结束循环.
实际效果是:
线程2输入数据后,线程1循环并未结束
这样的现象背后,其实涉及到了编译器的优化
在线程1的核心代码中,循环其实没干什么,只是在反复快速的执行循环条件中的比较操作
这里的比较会先从内存中读取flag的值到cpu中
然后会在CPU中比较这个值和0的相等关系
虽然读取内存比读取磁盘速度快很多,但是从CPU的寄存器上读取数据的速度要
比内存中数据还是要快很多
正因如此:
编译器判定这个逻辑中循环啥事也没干,只是频繁读取内存,于是编译器就把这个操作给优化了.
第一次把内存数据读取到CPU后,后续读内存并不是真的从内存中读取,而是直接取刚才CPU中读到的数据
但是这里是有问题的:
编译器认为flag没有改动,其实只是在当前线程中没有改动
编译器不能感知到其他线程是否对flag进行了修改,此时进行了误判
注意:编译器的优化,必须保证一个前提,优化后的逻辑和优化前的逻辑是
但是这个地方编译器的错误优化导致程序
这样的优化策略就是"内存可见性"
如果优化生效,内存就是不可见的了(其他线程修改了也看不见)
如果优化不生效,内存才是可见的(其他线程修改了就能看见了)
volatile关键字
作用:保持内存可见性:
禁止编译器进行刚才这种场景的优化(一个线程读,一个线程写,修改对于读线程来说可能没生效)
线程不安全的场景:
一个场景读 一个场景写 用volatile
两个线程写: 加锁解决线程安全问题
加了volatile之后,对这个内存的读取操作肯定是从内存中取,不加volatile的时候,读取操作可能是不从内存读取了而是直接读取CPU上次读到的旧的值.(当前代码会不会触发这样的编译器优化是不确定的)
因此解决方法是保持内存的可见性,只要在counter前加上volatile就可以防止编译器的错误优化
代码:
public class TestDemo {
static class Counter {
public volatile int counter = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(){
@Override
public void run() {
while(counter.counter == 0) {
}
System.out.println("循环结束");
}
};
Thread t2 = new Thread(){
@Override
public void run() {
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个整数");
counter.counter = sc.nextInt();
}
};
t1.start();
t2.start();
}
}
++count和count++本质上都是分三个步骤
1.在count的值从内存读到CPU中
2.在CPU中把这个数据+1
3.把结果写回原来的内存中
这个过程不论是前置++,后置++都是一样的,只要修改不是原子的,都有类似的问题
不是原子的:各种 +=, -=, /= ...
包括+ - ;
直接赋值操作不一定不是原子的,针对内置类型来说一般是原子的(64位CPU是的,
如果是针对32位CPU,针对long来赋值,其实是分两步,高32位和低32位是分两次来赋值的.)
引用类型不一定