影响线程安全问题的因素有很多
包括但不限于:
- 内存可见性
- 指令重排序
本篇将通过实例对上述原因进行讲解
🔎1.示例
🌻示例代码
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个步骤
- 1从内存读取数据n到寄存器
- 2比较寄存器中的值是否等于0
小知识
访问速度:寄存器>内存>硬盘
由于寄存器的访问速度大于内存,且循环体内部是空着的.
每次从内存读取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(); } }
运行结果
🔎2.示例
volatile还有另外一个作用,禁止指令重排序
那么什么是指令重排序呢
举个栗子
有一天
你的女朋友让你去超时帮她分别买(1)薯片(2)旺仔牛奶(3)QQ糖(4)曲奇饼干
这时你选择的路线是入口–>(1)薯片–>(4)曲奇饼干–>(3)QQ糖–>(2)旺仔牛奶–>出口
编译器就会帮你进行优化(指令重排序)(执行顺序入口–>(1)薯片–>(2)旺仔牛奶–>(4)曲奇饼干->(3)QQ糖–>出口)
此时如果加入volatile就可以让编译器不再帮你进行优化
🔎结尾
创作不易,如果对您有帮助,希望您能点个免费的赞👍
大家有什么不太理解的,可以私信或者评论区留言,一起加油