volatile 这个单词本意是易变的、易失的,那它和线程安全有什么关系呢?我们来看一段代码:
/**
* 创建2个线程去同时访问一个共享变量rmb,第1个线程进行余额消费,第2个线程进行账户冻结,
* 这里有个需求就是让第2个线程根据用户的输入来冻结账户,从而阻止线程1进行消费
*/
class Account{
//账户余额
private double rmb = Double.MAX_VALUE;
//账户状态
private int isUsable = 1; //1表示可用、2表示禁用
//账户对象
private static Account instance = new Account();
//这里是单例模式,所以将构造器私有化
private Account(){
}
public static Account getInstance(){
return instance;
}
public double getRmb(){
return rmb;
}
public void setRmb(double rmb){
this.rmb = rmb;
}
public int getIsUsable() {
return isUsable;
}
public void setIsUsable(int isUsable) {
this.isUsable = isUsable;
}
public void consume(){
rmb -= 100.0; //规定每次只能消费100
}
}
public class Demo {
public static void main(String[] args){
//账户对象
Account account = Account.getInstance();
//线程1进行消费
Thread t1 = new Thread( () -> {
while(account.getIsUsable() == 1){ //如果账户可以使用就循环进行消费
account.consume();
}
System.out.println("无法进行消费,账户被冻结!");
},"线程1");
t1.start();
//线程2进行账户冻结
Thread t2 = new Thread(() -> {
Scanner scan = new Scanner(System.in);
System.out.println("是否冻结账户?Y/N");
if("Y".equalsIgnoreCase(scan.next())){
account.setIsUsable(0);
}
},"线程2");
t2.start();
}
}
我们这里想得出的结果是在键盘输入y/Y之后,线程1检测到账户被冻结然后跳出循环打印提示信息。但是调试结果和我们预测的不一样,线程1并没有结束而是继续在运行。如下图:
这里的原因就涉及到Java内存可见性。
account.getIsUsable() == 1 对应的指令是两步:
- 把内存中变量的值读到CPU的寄存器中,称为load;
- 将寄存器中的值与1进行比较,根据比较结果决定下一步如何执行(条件跳转指令),称为cmp。
account.getIsUsable() == 1 作为循环判断的条件每秒钟可以执行百万次以上,站在Java编译器的视角,load操作相比cmp操作要慢的多,且这里一直在做重复读操作同时结果还都一样,因此为了提升执行效率,编译器做出了一个大胆的决定——不再重复的进行load了,将account.getIsUsable()获取到的变量值干脆直接放到CPU寄存器中,判断没有其他线程进行修改了。
这其实就是编译器优化在多线程的环境下产生了误判,一个线程对一个变量进行读取操作,同时另一个线程对这个变量进行修改,此时读到的值不一定是修改后的值,这个读线程没有感知到变量的变化,而实际上是有其他线程在修改的:
因此就需要程序员进行手动干预,我们通过给这个变量加上 volatile 关键字告诉编译器这个变量是易变的,你每次都需要从内存中去重新load这个变量的值,指不定啥时候就变了。
然后再重新执行程序,查看结果:
t2线程读取到键盘输入的确认指令后对账户状态进行修改,此时t1线程则知道了变量已被修改,跳出循环并打印提示信息。