一文了解java中volatile关键字

认识volatile

volatile关键字的作用有两个:变量修改对其他线程立即可见、禁止指令重排。

第二个作用我们后面再讲,先主要讲一下第一个作用。通俗点来说,就是我在一个线程对一个变量进行了修改,那么其他线程马上就可以知道我修改了他。嗯?难道我修改了数值其他线程不知道?我们先从实例代码中来感受volatile关键字的第一个作用。

 private static  boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (!flag) {
            }
            System.out.println("Flag is now true!");
        }).start();

        Thread.sleep(1000);

        flag = true;
        System.out.println("Flag is set to true!");
    }

结果:

        

在没有volatile关键字的时候只展示了主线程中flag改变的值并输出结果,子线程并没有打印任何信息,说明主线程的改变不会对子线程有影响,他们之间是不可见的

现在我们加上volatile关键字:

private static volatile  boolean flag = false;

结果:

现在主线程的改变子线程也是可以看到flag的变化了,说明volatile让两个线程彼此可见了,同时也是有顺序的,这就是volatile关键字的第一个重要作用:变量修改对其他线程立即可见。关于指令重排我们后面再讲。

那么为什么变量的修改是对其他线程不是立即可见呢?volatile为何能实现这个效果?那这样我们可不可以每个变量都给他加上volatile关键字修饰?要解决这些问题,我们得先从Java内存模型说起。系好安全带,我们的车准备开进底层原理了。

Java内存模型

JVM把内存总体分为两个部分:线程私有以及线程共享,线程共享区域也称为主内存。线程私有部分不会发生并发问题,所以主要是关注线程共享区域。这个图大致上可以这么理解:

  1. 所有共享变量存储在主内存
  2. 每条线程拥有自己的工作内存
  3. 工作内存保留了被该线程使用的变量的主内存副本
  4. 变量操作必须在工作内存进行
  5. 不同线程之间无法访问对方的工作内存

简单总结一下,所有数据都要放在主内存中,线程要操作这些数据必须要先拷贝到自己的工作内存,然后只能对自己工作内存中的数据进行修改;修改完成之后,再写回主内存。线程无法访问另一个线程的数据,这也就是为什么线程私有的数据不存在并发问题。

那为什么不直接从主内存修改数据,而要先在工作内存修改后再写回主内存呢?这就涉及到了高速缓冲区的设计。简单来说,处理器的速度非常快,但是执行的过程中需要频繁在内存中读写数据,而内存访问的速度远远跟不上cpu的速度,导致降低了cpu的效率。因而设计出高速缓冲区,处理器可以直接操作高速缓冲区的数据,等到空闲时间,再把数据写回主内存,提高了性能。而JVM为了屏蔽不同的平台对于高速缓冲区的设计,就设计出了Java内存模型,来让开发者可以面向统一的内存模型进行编程。可以看到,这里的工作内存就对应硬件层面的高速缓冲区。

指令重排

前面我们一直没讲指令重排,是因为这是属于JVM在后端编译阶段进行的优化,而在代码中隐藏的问题很难去复现,也很难通过代码执行来看出差别。指令重排是即时编译器对于字节码编译过程中的一种优化,受到运行时环境的影响。限于能力,笔者只能通过理论分析来讲这个问题。

我们知道JVM执行字节码一般有两种方式:解释器执行和即时编译器执行。解释器这个比较容易理解,就是一行行代码解释执行,所以也不存在指令重排的问题。但是解释执行存在很大的问题:解释代码需要耗费一定的处理时间、无法对编译结果进行优化,所以解释执行一般在应用刚启动时或者即时编译遇到异常才使用解释执行。而即时编译则是在运行过程中,把热点代码先编译成机器码,等到运行到该代码的时候就可以直接执行机器码,不需要进行解释,提高了性能。而在编译过程中,我们可以对编译后的机器码进行优化,只要保证运行结果一致,我们可以按照计算机世界的特性对机器码进行优化,如方法内联、公共子表达式消除等等,指令重排就是其中一种。

计算机的思维跟我们人的思维是不一样的,我们按照面向对象的思维来编程,但计算机却必须按照“面向过程”的思维来执行。所以,在不影响执行结果的情况下,JVM会更改代码的执行顺序。注意,这里的不影响执行结果,是指在当前线程下。如我们可能会在一个线程中初始化一个组件,等到初始化完成则设置标志位为true,然后其他线程只需要监听该标志位即可监听初始化是否完成,如下:

// 初始化操作
isFinish = true;

在当前线程看起来,注意,是看起来,会先执行初始化操作,再执行赋值操作,因为结果是符合预期的。但是!!!在其他线程看来,这整个执行顺序都是乱的。JVM可能先执行isFinish赋值操作,再执行初始化操作;而如果你在别的线程监听isFinish变化,就可能出现还未初始化完成isFinish却是true的问题。而volatile可以禁止指令重排,保证在isFinish被赋值之前,所有的初始化动作都已经完成

volatile的具体定义

上面讲了两个看起来跟我们的主角volatile关系不大的知识点,但其实是非常重要的知识点。

首先,通过Java内存模型的理解,现在知道为什么会出现线程对变量的修改其他线程未立即可知的原因了吧?线程修改变量之后,可能并不会立即写回主内存,而其他线程,在主内存数据更新后,也并不会立即去主内存获取最新的数据。这也是问题所在。

被volatile关键字修饰的变量规定:每次使用数据都必须去主内存中获取;每次修改完数据都必须马上同步到主内存。这样就实现了每个线程都可以立即收到该变量的修改信息。不会出现读取脏数据旧数据的情况。

第二个作用是禁止指令重排。这里是使用到了JVM的一个规定:同步内存操作前的所有操作必须已经完成。而我们知道每次给volatile赋值的时候,他会同步到主内存中。所以,在同步之前,保证所有操作都必须完成了。所以当其他线程监测到变量的变化时,赋值前的操作就肯定都已经完成了。

既然volatile关键字这么好,那可不可以每个地方都使用好了?当然不行!在前面讲Java内存模型的时候有提到,为了提高cpu效率,才分出了高速缓冲区。如果一个变量并不需要保证线程安全,那么频繁地写入和读取内存,是很大的性能消耗。因而,只有必须使用volatile的地方,才使用他。

volatile修饰的变量一定是线程安全吗

首先明确一下,怎么样才算是线程安全?

  1. 一个操作要符合原子性,要么不执行,要么一次性执行完成,在执行过程中不会受其他操作的影响。
  2. 对于变量的修改相对于其他线程必须立即可见。
  3. 代码的执行在其他线程看来要满足有序性。

从上面分析我们讲了:代码的有序性、修改的可见性,但是,缺少了原子性。在JVM中,在主内存进行读写都是满足原子性的,这是JVM保证的原子性,那么volatile岂不是线程安全的?并不是。

JVM的计算过程并不是原子性的。我们举个例子,看一下代码:

public class VolatileDemo {
    private volatile int counter = 0;

    public void increment() {
        counter++;
    }

    public int getCounter() {
        return counter;
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileDemo demo = new VolatileDemo();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                demo.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                demo.increment();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Counter: " + demo.getCounter());
    }
}

 结果应该是2000000才对,让我们看下结果:

结果竟然不对?

这是因为:volatile仅仅只是保证了修改后的数据对其他线程立即可见,但是并不保证运算的过程的原子性。

这里的counter++在编译之后是分为三步:1.在工作区中取出变量数据到处理器 2.对处理器中的数据进行加一操作 3.把数据写回工作内存。如果,在变量数据取到处理器运算的过程中,变量已经被修改了,所以这时候进行自增操作得到的结果就是错误的。举个例子:

变量a=5,当前线程取5到处理器,这时a被其他线程改成了8,但是处理器继续工作,把5自增得到6,然后把6写回主内存,覆盖数据8,从而导致了错误。因此,由于Java运算的非原子性,volatile并不是绝对线程安全

那什么时候他是安全的:

  1. 计算的结果不依赖原来的状态。
  2. 不需要与其他的状态变量共同参与不变约束。

通俗点来讲,就是运算不需要依赖于任何状态的运算。因为依赖的状态,可能在运算的过程中就已经发生了变化了,而处理器并不知道。如果涉及到需要依赖状态的运算,则必须使用其他的线程安全方案,如加锁,来保证操作的原子性。

示例:

适用场景

状态标志/多线程通知

状态标志是很适合使用volatile关键字,正如我们在第一部分举的例子,通过设置标志来通知其他所有的线程执行逻辑。或者是如上一部分的例子,当初始化完成之后设置一个标志通知其他线程。

而更加常用的一个类似场景是线程通知。我们可以设置一个变量,然后多个线程观察他。这样只需要在一个线程更改这个数值,那么其他的观察这个变量的线程均可以收到通知。非常轻量级、逻辑也更加简单。

保证初始化完整:双重锁检查问题

单例模式是我们经常使用的,其中一种比较常见的写法就是双重锁检查,如下:

public class JavaClass {
	// 静态内部变量
   private static JavaClass javaClass;
    // 构造器私有
   private JavaClass(){}
    
   public static JavaClass getInstance(){
       // 第一重判空
       if (javaClass==null){
           // 加锁,第二重判空
           synchronized(JavaClass.class){
               if (javaClass==null){
                   javaClass = new JavaClass();
               }
           }
       }
       return javaClass;
   }
}

这种代码很熟悉,对吧,具体的设计逻辑我就不展开了。那么这样的代码是否绝对是线程安全的呢?并不是的,在某些极端情况下,仍然会出现问题。而问题就出在javaClass = new JavaClass();这句代码上。

新建对象并不是一个原子操作,他主要有三个子操作:

  1. 分配内存空间
  2. 初始化Singleton实例
  3. 赋值 instance 实例引用

正常情况下,也是按照这个顺序执行。但是JVM是会进行指令重排优化的。就可能变成:

  1. 分配内存空间
  2. 赋值 instance 实例引用
  3. 初始化Singleton实例

赋值引用在初始化之前,那么外部拿到的引用,可能就是一个未完全初始化的对象,这样就造成问题了。所以这里可以给单例对象进行volatile修饰,限制指令重排,就不会出现这种情况了。当然,关于单例模式,更加建议的写法还是利用类加载来保证全局单例以及线程安全,当然,前提是你要保证只有一个类加载器。限于篇幅这里就不展开了。

总结:

适用volatile的场景一般是初始化标志

不使用volatile的场景涉及指令重排,多线程操作数字累加

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/244799.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python列表的循环遍历

数据容器&#xff1a;一个可以存储多个元素的Python数据类型 有哪些数据容器&#xff1a;list&#xff08;列表&#xff09;&#xff0c;tuple&#xff08;元组&#xff09;&#xff0c;str&#xff08;字符串&#xff09;&#xff0c;set&#xff08;集合&#xff09;&#x…

基于人工智能算法与视频监控相结合的EasyCVR智能游乐园监控方案

随着圣诞节的到来&#xff0c;人们都已经在规划如何安排平安夜活动&#xff0c;游乐园俨然成为了人们的首选。游乐园人员流量大且密集&#xff0c;特别是在节假日和重大节日&#xff0c;人满为患&#xff0c;极易发生事故&#xff0c;为保证游乐场安全运营&#xff0c;减少事故…

最新鸿蒙HarmonyOS4.0开发登陆的界面2

登陆功能 代码如下&#xff1a; import router from ohos.router; Entry Component struct Index {State message: string XXAPP登陆State userName: string ;State password: string ;build() {Row() {Column({space:50}) {Image($r(app.media.icon)).width(200).interpol…

思科模拟器Cisco Packet Tracer 8.2.1注册、下载和安装教程(正确+详细)

思科模拟器的注册、下载和安装 1、思科官方的注册地址&#xff1a;https://www.cisco.com/c/zh_cn/index.html在该网址注册思科账号&#xff0c;但是这个注册的账户不能登录思科模拟器 Cisco Packet Tracer 2、思科学院的注册&#xff08;不用&#xff09;国外地址&#xff1…

大模型应用_PrivateGPT

https://github.com/imartinez/privateGPT 1 功能 整体功能&#xff0c;想解决什么问题 搭建完整的 RAG 系统&#xff0c;与 FastGPT相比&#xff0c;界面比较简单。但是底层支持比较丰富&#xff0c;可用于知识库的完全本地部署&#xff0c;包含大模型和向量库。适用于保密级…

大模型应用_chuanhu川虎

https://github.com/GaiZhenbiao/ChuanhuChatGPT 1 功能 整体功能&#xff0c;想解决什么问题 官网说明&#xff1a;为ChatGPT等多种LLM提供了一个轻快好用的Web图形界面和众多附加功能 当前解决了什么问题&#xff0c;哪些问题解决不了 支持多种大模型&#xff08;也可接入本…

第六节JavaScript this、let、const关键字

一、JavaScript this关键字 1、描述 面向对象语言中&#xff0c;this表示当前对象的一个引用。 但在JavaScript中&#xff0c;this不是固定不变的&#xff0c;它会随着执行环境的改变而变化。 方法中&#xff0c;this表示该方法所属的对象。如果单独使用&#xff0c;this表…

[Halcon图像] 基于多层神经网络MLP分类器的思想提取颜色区域

&#x1f4e2;博客主页&#xff1a;https://loewen.blog.csdn.net&#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;本文由 丶布布原创&#xff0c;首发于 CSDN&#xff0c;转载注明出处&#x1f649;&#x1f4e2;现…

排序的简单理解(上)

1. 排序的概念及引用 1.1 排序的概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作&#xff08;按照我们的需求能够有序的将数据信息排列起来&#xff09;。 稳定性&#xff1a;假…

shiro入门demo(一)身份验证

shiro&#xff08;身份&#xff09;认证&#xff0c;简单来说就是登录/退出。搭建springboot项目&#xff0c;引入shiro和单元测试依赖&#xff1a; <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-…

Nacos-NacosRule 负载均衡—设置集群使本地服务优先访问

userservice: ribbon: NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则 NacosRule 权重计算方法 目录 一、介绍 二、示例&#xff08;案例截图&#xff09; 三、总结 一、介绍 NacosRule是AlibabaNacos自己实现的一个负载均衡策略&…

【Spark精讲】Spark Shuffle详解

目录 Shuffle概述 Shuffle执行流程 总体流程 中间文件 ShuffledRDD生成 Stage划分 Task划分 Map端写入(Shuffle Write) Reduce端读取(Shuffle Read) Spark Shuffle演变 SortShuffleManager运行机制 普通运行机制 bypass 运行机制 Tungsten Sort Shuffle 运行机制…

mysql EXPLAIN命令的输出列简介

MySQL :: MySQL 8.2 Reference Manual :: 8.8.2 EXPLAIN Output Format explain命令提供了mysql数据库如何执行SQL语句的信息&#xff0c;可以跟 SELECT, DELETE, INSERT, REPLACE, UPDATE, 和 TABLE一起使用。 explain命令可能输出多行&#xff0c;每行涉及一个表 。 先来看…

数据之美:零售业的变革之道

数据可视化能够为零售业带来令人瞩目的变化。随着零售业务的发展&#xff0c;数据可视化成为了洞察市场、优化运营并提升客户体验的强大工具。下面我就以可视化从业者的视角出发&#xff0c;简单分析一下数据可视化为零售业可能带来的改变。 数据可视化让零售商深入了解消费者行…

LeetCode(59)反转链表 II【链表】【中等】

目录 1.题目2.答案3.提交结果截图 链接&#xff1a; 反转链表 II 1.题目 给你单链表的头指针 head 和两个整数 left 和 right &#xff0c;其中 left < right 。请你反转从位置 left 到位置 right 的链表节点&#xff0c;返回 反转后的链表 。 示例 1&#xff1a; 输入&am…

【为什么POI的SXSSFWorkbook占用内存更小?】

&#x1f513;为什么POI的SXSSFWorkbook占用内存更小&#xff1f; &#x1f3c6;POI的SXSSFWorkbook&#x1f3c6;POI的SXSSFWorkbook占用内存&#x1f3c6;扩展配置行缓存限制 &#x1f3c6;POI的SXSSFWorkbook SXSSFWorkbook类是Apache POI库的一部分&#xff0c;它是一个流…

第五节JavaScript typeof、类型转换与正则表达式

一、typeof、null和undefined 1、typeof操作符 使用typeof操作符来检测变量的数据类型。 实例&#xff1a; <!DOCTYPE html> <html><head><meta charset"utf-8"><title>JavaScript基础知识学习</title></head><bod…

单元测试二(实验)-云计算2023.12-云南农业大学

1、实践系列课《深入浅出Docker应用》 https://developeraliyun.com/adc/scenarioSeries/713c370e605e4f1fa7be903b80a53556?spma2c6h.27088027.devcloud-scenarioSeriesList.13.5bb75b8aZHOM2w 容器镜像的制作实验要求 创建Dockerfile文件: FROM ubuntu:latest WORKDIR data…

C++初阶(十六)优先级队列

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 文章目录 一、priority_queue的介绍和使用1、priority_queue的介绍2、priority_queue的使用 二、priori…

高性能国产TYPE-C/DP/EDP转MIPIDSI/CSI/LVDS,龙迅LT7911D,支持高达4K60HZ的分辨率

LT7911D概述&#xff1a; T7911D是一款高性能TYPE-C/DP/EDP转2 PORT MIPI或者LVDS的芯片&#xff0c;目前主要在AR/VR或者显示器上应用的很多&#xff0c;对于DP1.2输入&#xff0c;LT7911D可配置为1/2/4车道。自适应均衡化使其适用于长电缆应用&#xff0c;最大带宽可达21.6G…