内存可见性
影响线程安全的原因有很多,内存可见性的也会引起线程不安全。
以下面的案例来看,线程启动后,t1不断进行循环,直到t2输入数字后改变状态,t1线程才会结束。
private static int count;
public static void main(String[] args) {
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:");
count = scanner.nextInt();
System.out.println("count = "+count);
});
t1.start();
t2.start();
}
通过下面的打印我们可以知道,线程t1并没有执行结束,仍然处于while循环中。那么为什么count值改变了,结果却没有发生改变。这就涉及到了内存可见性的问题。
内存可见性原因
在上面的代码中,由于循环体中没有内容,线程t1的while循环中需要执行两个操作。
第一个是load操作,即从内存中读取count数据到cpu寄存器中;
第二个是compare操作,即进行count==0的判断操作。如果条件成立,则会顺序执行,条件不成立,就会跳出while循环执行后续代码。
前置条件: 这段循环体中不存在代码,只能反复执行load和compare操作。
1)在循环操作过程中load比compare指令所花费的时间多上许多,一次load操作所花费的时间足够compare执行成千上万次。
2)在JVM中发现load操作每次执行的结果都是相同的(在线程t2修改count值之前已经经过许多次load了)
优化操作: 在发现这样的操作十分的无效以后,JVM开始了他的优化操作:在第一次真正执行了load操作以后,JVM后续继续执行后面的代码以后都不进行load操作了,而是直接读取一开始的load值。
后果: 经过JVM的优化操作,后续的count值即使修改了也无法更新,这也就导致了while循环无法结束。
volatile
为了防止JVM的自作聪明引起的祸端,于是创建了volatile关键字,即反复无常。提醒JVM不可以对带有这个关键字的变量进行优化。
private volatile static int count;
对count变量添加volatile以后,再执行代码,我们可以成功修改count值并更新到while循环中。
IO操作
在线程t1中,因为while循环中没有语句,最后导致了load操作被优化。如果我们在while循环中添加了打印语句,load操作是否还会被优化?
结果:在执行了下面的代码以后,修改count值是可以停止while循环的。
**原因:**我们知道,load操作是因为过程浪费资源且没有改变才会被优化,而在while循环中,打印的IO操作所耗费的资源比load操作要多得多,并且每次的IO操作所带来的结果是不相同的,于是就形成了 “load操作浪费,但IO操作所花费的资源更多” 。因此load操作没有被优化。
private volatile static int count;
public static void main(String[] args) {
Thread t1 = new Thread(()-> {
while (count == 0) {
System.out.println("t1");
}
System.out.println("t1执行结束");
});
Thread t2 = new Thread(()-> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入count:");
count = scanner.nextInt();
System.out.println("count = "+count);
});
t1.start();
t2.start();
}
总结
volatile关键字的出现是专门针对内存可见性的场景来解决问题的,并不能解决多线程中多个线程修改同一变量的问题。虽然使用加锁操作也可以在一定程度上解决内存可见性的问题,但加锁所耗费的资源比volatile多得多。
源码☞内存可见性源码