首页 > 系统相关 >【多线程-从零开始-伍】volatile关键字和内存可见性问题

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

时间:2024-08-06 20:53:22浏览次数:18  
标签:t1 编译器 从零开始 volatile 内存 JVM 线程 多线程 优化

volatile 关键字

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 线程仍是持续工作的image.png|353
  • 上述问题的原因,就是“内存可见性问题

内存可见性问题

层次空间速度成本数据
CPU 寄存器掉电后丢失
内存中等中等中等掉电后丢失
硬盘掉电后不丢失
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 操作

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

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

volatile 关键字的用法

  • volatile 关键字修饰一个变量,提示编译器说这个变量是“易变”的
  • 编译器进行上述优化的前提,是编译器认为,针对这个变量的频繁读取,结果都是固定的
  • 使用 volatile 关键字修饰变量之后,编译器就会禁止上述的优化,确保每次循环都是从内存中重新读取数据
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 的工作内存未产生影响,从而出现了上述内存可见性问题

标签:t1,编译器,从零开始,volatile,内存,JVM,线程,多线程,优化
From: https://blog.csdn.net/Yeeear/article/details/140965963

相关文章

  • 编程深水区之并发④:Web多线程
    Node的灵感来源于Chrome,更是移植了V8引擎。在Node中能够实现的多线程,在Web环境中自然也可以。一、浏览器是多进程和多线程的复杂应用在本系列的第二章节,有提到现代浏览器是一个多进程和多线程的复杂应用。浏览器主进程统管全局,每个Tab页都会创建一个渲染子进程,同时还有G......
  • 异步编程和多线程有
    在C#中,多线程和异步编程是两个相关但不完全相同的概念。下面我会解释这两个概念的区别,并给出一些常见的问题及解答。多线程vs异步编程多线程:多线程指的是在一个进程中创建多个线程来并行执行任务。多线程可以用来处理计算密集型任务,充分利用多核处理器的计算能力。多......
  • 从零开始学嵌入式技术之C语言11:指针
    一:指针的理解(1)变量的访问方式        内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 ,为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元通常占用1个字节。变量在内存中分配空间,不同类型的变量占用不同大小的空间,那如何访问内......
  • 认识多线程
    一.认识线程(Thread)1.1)线程是什么线程(Thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。线程是独立调度的基本单位,在进程中有多个线程同时执行时,可以显著提高程序的运行效率。一个进程可以拥有多个线程,这些线程共享进程的资源(如内......
  • 多线程
    多线程Threadpackagelesson01;//创建线程的方式一:继承Thread类,重写run()方法,调用start开启线程//线程开启不一定立即执行,有CPU调度执行publicclassTestThreadextendsThread{@Overridepublicvoidrun(){super.run();//run方法线程体for......
  • 【从零开始一步步学习VSOA开发】创建VSOA的server端
    创建VSOA的server端创建工程参考hellovsoa工程,创建server工程,工程源码修改如下:#include<stdio.h>#include<stdlib.h>#include<string.h>#include<netinet/in.h>#include<arpa/inet.h>#include"vsoa_platform.h"#include"vsoa_ser......
  • 多线程-进阶2
     博主主页: 码农派大星.  数据结构专栏:Java数据结构 数据库专栏:MySQL数据库JavaEE专栏:JavaEE关注博主带你了解更多数据结构知识1.CAS1.1CAS全称:Compareandswap比较内存和cpu寄存器中的内容,如果发现相同,就进行交换(交换的是内存和另一个寄存器的内容)......
  • JavaEE 第1节 认识多线程
    本节目标(全是重点,都必须掌握)1、了解什么是线程、多线程、进程以及他们之间的关系2、了解多线程的优势以及各种特性3、用Java掌握多种创建线程的方法一、线程、多线程、进程1、概念1.基本概念这三个名词的概念可以用一个餐馆的厨房和厨师来进行超级形象的比喻。想象一下......
  • NLP从零开始------7基础文本处理之关键词提取
    1.关键词提取技术简介    在现代。文本是海量的信息中量最大的、使用最广泛的一种数据类型。这些信息数据虽然能为人们的生活提供便利。但是在提取有价值的信息时仍面临着困难。通过关键词提取可以快速地提取一篇新闻的关键信息。    关键词是能够反应文本主......
  • 从零开始的JAVAday29~day35
    后续语法if()语法若满足()中的语法,则执行后面的语句。循环for(a;b;c)和while(c)语法for(a;c;b)语法意思为在循环前进行a语句每次循环结束后进行b语法,若满足c语句则再次循环。whlie(c)循环若满足c条件则循环。......