影响线程安全问题的因素有很多
包括但不限于:
- 内存可见性
- 指令重排序
本篇将通过实例对上述原因进行讲解
import java.util.Scanner;
public class Test {
public static int n = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(n == 0) {
//空着
}
System.out.println("线程结束运行");
},"这是t1线程");
Thread t2 = new Thread(() -> {
Scanner scan = new Scanner(System.in);
System.out.println("请输入一个不等于0的数字终止线程:");
n = scan.nextInt();
});
t1.start();
t2.start();
}
}
代码描述:
- 上述代码启动了 t1,t2两个线程
- t1线程的while()循环体内部是空着的
- 通过t2线程修改n的值让while()的循环条件不满足,从而让线程t1结束运行
那么这样做能否成功呢?
答案是不能
通过运行结果我们看到 t1线程仍然处于运行状态
注意这里的while(n==0)
此处需要执行2个步骤
小知识
访问速度:寄存器>内存>硬盘
由于寄存器的访问速度大于内存,且循环体内部是空着的.
每次从内存读取n的值到寄存器,读取的结果是相同的(由于循环体是空着的,所以几乎不占用时间执行循环体里的操作,所以在较短的时间内读取了多次n的值,发现n的值还是0).
读取操作(内存)相对于比较操作(寄存器)是一个比较大的时间开销,编译器就默认帮我们进行了优化
这也就解释了为什么 t2线程修改n的值t1线程没能停下来
上述现象可以解释为内存可见性的缘故
所谓内存可见性,就是多线程环境下,编译器对代码进行了优化,产生了误判,从而引起了bug
那么如果循环体里不是空着的, t1线程是不是就会停下来了呢?
答案是对的
当循环体非空时,读取操作就不再被看作是一个比较大的时间开销,编译器也就不再帮忙进行优化了
当循环体为空时,可以加入volatile避免编译器进行优化
直接访问工作内存(寄存器), 速度非常快, 但是可能出现数据不一致的情况.
加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了
也就是说,volatile 能保证内存可见性
完整代码
public class Test2 {
volatile public static int n = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(n == 0) {
//空着
/*try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("循环体非空");*/
}
System.out.println("线程结束运行");
},"这是t1线程");
Thread t2 = new Thread(() -> {
Scanner scan = new Scanner(System.in);
System.out.println("请输入一个不等于0的数字终止线程:");
n = scan.nextInt();
});
t1.start();
t2.start();
}
}
运行结果
volatile还有另外一个作用,禁止指令重排序
那么什么是指令重排序呢
举个栗子
有一天
你的女朋友让你去超时帮她分别买(1)薯片(2)旺仔牛奶(3)QQ糖(4)曲奇饼干
这时你选择的路线是入口–>(1)薯片–>(4)曲奇饼干–>(3)QQ糖–>(2)旺仔牛奶–>出口
编译器就会帮你进行优化(指令重排序)(执行顺序入口–>(1)薯片–>(2)旺仔牛奶–>(4)曲奇饼干->(3)QQ糖–>出口)
此时如果加入volatile就可以让编译器不再帮你进行优化
创作不易,如果对您有帮助,希望您能点个免费的赞
大家有什么不太理解的,可以私信或者评论区留言,一起加油