【多线程-从零开始-伍】volatile关键字和内存可见性问题

volatile 关键字

java 复制代码
import java.util.Scanner;  
  
public class Demo2 {  
    private static int n = 0;  
  
    public static void main(String[] args) {  
        Thread t1 = new Thread(() -> {  
            while(n == 0){  
                //啥都不写  
            }  
            System.out.println("t1 线程结束循环");  
        }, "t1");  
        Thread t2 = new Thread(() -> {  
            Scanner scanner = new Scanner(System.in);  
            System.out.println("请输入一个整数:");  
            n = scanner.nextInt();  
        }, "t2");        
        t1.start();  
        t2.start();  
    }
}
  • 当我们输入一个非 0 的数,理应 t1 中循环条件就不成立,将会打印"线程结束循环",但实际上输入 1 后,t1 没有任何动静
  • 我们通过 jconsole 可以看到 t1 线程仍是持续工作的
  • 上述问题的原因,就是"内存可见性问题"

内存可见性问题

层次 空间 速度 成本 数据
CPU 寄存器 掉电后丢失
内存 中等 中等 中等 掉电后丢失
硬盘 掉电后不丢失
java 复制代码
while(n == 0) {
	
}
  • 上面代码中的这个操作,循环会执行非常多次,每次循环,都要执行一个 n == 0 这样的判定
    1. 从内存读取数据到寄存器中(读取内存,相比之下,这个操作的速度非常慢)
    2. 通过类似 cmp 指令,比较寄存器和 0 的值(这个指令执行速度非常快)
  • 此时 JVM 执行这个代码的过程的时候,发现:每次执行循环操作的开销非常大,并且每次执行的结果都是一样的
  • 并且 JVM 根被没有意识到,用户可能在未来会修改 n,于是 JVM 就做了一个大胆的操作------直接把这个操作给优化掉了
    • 每次循环,不会重新读取内存中的数据,而是直接读取寄存器/cache 中的数据(缓存的结果)
  • JVM 做出上述决定之后,此时意味着循环的开销大幅度降低了,但当用户修改 n 的时候,内存中的 n 已经改变了,但是由于 t1 线程每次循环,不会真的读内存,所以感知不到 n 的改变
  • 内存中的 n 的改变,对于线程 t1 来说是"不可见的",这样就引起了 bug
  • 内存可见性问题本质上是编译器/JVM 对代码进行优化的时候,优化出了 bug
  • 如果代码是单线程的,编译器/JVM 的代码优化一般都是非常准确的,优化之后,不会影响到逻辑
  • 但是代码如果是多线程的,编译器/JVM 的代码优化就可能出现误判(编译器/JVM 的 bug),导致不该优化的地方也给优化了,于是就造成了内存可见性问题

!quote\] ***编译器问啥要做优化?*** * 有些程序员写出来的代码太低效了,为了能降低程序员的门槛,即使你的代码写的一般,最终执行也不会落下风 * 因此一些主流的编译器,都会好引入优化机制(优化手段是多种多样的) * 优化就是编译器自动调整你写的代码,保持原有逻辑不变的前提下,提高代码的执行效率 * 代码优化的效果是非常明显的 *** ** * ** *** * 若一个服务器在开启优化的时候启动时间为 10 min,那么在不开启优化的时候,启动时间可能会在 30 min+


若在 while 循环中加入一个 sleep 操作

java 复制代码
while(n == 0) {
	Thread.sleep(10);
}
System.out.println("t1 线程结束循环");

//在输入1后,成功输出:"t1 线程结束循环"
  • 说明加入 sleep 之后,刚才谈到的针对读取 n 内存数据的优化操作不再进行了
  • 因为和读取内存相比,sleep 的开销更大,远远超过了读取内存,就算把读取内存的操作优化掉,也没有意义,杯水车薪

volatile 关键字的用法

  • volatile 关键字修饰一个变量,提示编译器说这个变量是"易变"的
  • 编译器进行上述优化的前提,是编译器认为,针对这个变量的频繁读取,结果都是固定的
  • 使用 volatile 关键字修饰变量之后,编译器就会禁止上述的优化,确保每次循环都是从内存中重新读取数据
java 复制代码
private static volatile int n = 0;
  • 编译器的开发者,知道这个场景中可能出现误判,于是就把权限交给程序员,让程序员能够部分的干预到优化的进行
  • 引入 volatile 的时候,编译器生成这个代码的时候,就会给这个变量的读取操作附近生成一些特殊的指令,称为"内存屏障 ",后续 JVM 执行到这些特殊指令,就知道不能进行上述优化了
    volatile 只是解决内存可见性问题,不能解决原子性问题,如果两个线程针对同一个变量进行修改(count++),volatile 也无能为力

!quote\] 网络上"内存可见性"问题: * 工作内存(其实就是 CPU 的寄存器和 cache) * 主内存 *** ** * ** *** * 整个 Java 程序持有这个主内存,每个 Java 程序又有一份自己的工作内存 * 像上述例子中的内存变量 n,本身是在主内存中,在 t1 和 t2 线程工作的过程中,就会把主内存的数据拷贝到\>工作内存中 * t2 如果修改了 n,先修改工作内存,再写回到主内存中。t1 读取 n 的时候,则是从主内存加载到工作内存,接下来的判定都是依照工作内存的值来进行判定的。此时 t2 修改了主内存,对于 t1 的工作内存未产生影响,从而出现了上述内存可见性问题

相关推荐
艾迪的技术之路10 分钟前
redisson使用lock导致死锁问题
java·后端·面试
今天背单词了吗98028 分钟前
算法学习笔记:8.Bellman-Ford 算法——从原理到实战,涵盖 LeetCode 与考研 408 例题
java·开发语言·后端·算法·最短路径问题
天天摸鱼的java工程师31 分钟前
使用 Spring Boot 整合高德地图实现路线规划功能
java·后端
东阳马生架构1 小时前
订单初版—2.生单链路中的技术问题说明文档
java
咖啡啡不加糖1 小时前
暴力破解漏洞与命令执行漏洞
java·后端·web安全
风象南1 小时前
SpringBoot敏感配置项加密与解密实战
java·spring boot·后端
DKPT1 小时前
Java享元模式实现方式与应用场景分析
java·笔记·学习·设计模式·享元模式
Percep_gan1 小时前
idea的使用小技巧,个人向
java·ide·intellij-idea
缘来是庄1 小时前
设计模式之迭代器模式
java·设计模式·迭代器模式