多线程篇四——volatile关键字

如笔者理解有误,欢迎交流指正⭐

首先我们要明晰的一点是计算机运行程序或代码时经常要访问数据,这些依赖的数据往往存储在内存中.
CPU使用变量的时候需要先从内存中读取出来放到CPU的寄存器中|由此我们可得读内存比读硬盘快,读寄存器比读内存快,但是CPU一旦涉及读/写内存速率很低.此时我们聪明得编译器就会帮忙了(虽然有时候会帮倒忙) 编译器可能会对代码做出优化,减少读内存的次数。提高整体的效率.

volatile能保证内存可见性

上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.Scanner;

public class Demo14 {
public static int flg = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(flg == 0) {
//啥也没有
}
System.out.println("game over!")
});
Thread t2 = new Thread(()-> {
Scanner sc = new Scanner(System.in);
flg = sc.nextInt();
});
t1.start();
t2.start();
}
}

运行发现
img
当我输入6的时候,并没有输出”game over!” 按理来说修改flg的值不为0的时候会输出”game over!”为什么会出现异常呢?
这就是我们的内存可见性问题.
读取flg的值这个过程分为两步走.

1
2
1.load读取内存中的flg值到寄存器中.
2.通过cmp指令比较器比较缓存器的值是否为0,再决定是否要进入循环.

乍一看这么处理不也没什么问题。但是,while循环此时飞速运转短时间内会进行大量重复的操作,这样就出现了最开头介绍的小聪明编译器.big 胆就水灵灵优化上了😀
只在第一次循环的时候读内存后续直接从寄存器中取出数据
由于修改flg是t2线程非t1线程本身,编译器没有正确的判定(提高效率不保证逻辑不变哈)以为flg未被修改就直接启动优化了,t1感受不到修改过后的flg的内存变化
如何避免小聪明不要帮倒忙?这就需要聪明的你使用volatile关键字提醒编译器.【强制爱读取内存】
被volatile修饰的变量的的读写操作相关指令,是不可以重排序的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.Scanner;

public class Demo14 {
public static volatile int flg = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(flg == 0) {
//啥也没有
}
System.out.println("game over!");
});
Thread t2 = new Thread(()-> {
Scanner sc = new Scanner(System.in);
flg = sc.nextInt();
});
t1.start();
t2.start();
}
}

当然解决内存可见性问题的方法出了从根部抹杀(volatile关键字)还可以规避问题(sleep延缓开销速率 优化也没必要进行了)

volatile不保证原子性

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.