一文了解java中volatile关键字

news2024/12/27 11:31:01

认识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的场景涉及指令重排,多线程操作数字累加

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

树莓派zero w入坑指南

树莓派zero w入坑指南 入坑契机 说起创客不得不提到开源硬件Raspberry Pi(树莓派)。它是一款基于ARM的微型电脑主板&#xff0c;以MicroSD卡为硬盘&#xff0c;提供HDMI和USB等外部接口&#xff0c;可连接显示器和键鼠。以上部件全部整合在一张仅比信用卡稍大的主板上&#x…

MHA高可用实验(故障模拟+恢复)

实验前准备 MHA manager节点服务器&#xff1a;192.168.188.13 MHA node和manager组件 Master节点服务器&#xff1a;192.168.188.14 mysql5.7、MHA node组件 Slave节点服务器1&#xff1a;192.168.188.15 mysql5.7、MHA node组件 Slave节点服务器2&#xff1a;192.…

uniapp x 相比于其他的开发系统框架怎么样?

首先我们要知道niapp这是一种基于Vue.js开发的跨平台应用框架&#xff0c;可以将同一套代码同时运行在多个平台上&#xff0c;包括iOS、Android、H5等。相比其他开发系统框架&#xff0c;他有什么优点呢&#xff1f;让我们共同探讨一下吧&#xff01; 图片来源&#xff1a;unia…

python每日学10:关于python实用版本的选择

用python也有好几年了&#xff0c;也会经常安装python&#xff0c;因为有工作需要&#xff0c;可能在各个地方使用python&#xff0c;自己的电脑也经常重装&#xff0c;重装后会装python&#xff0c;还有的时候&#xff0c;装的包太多了&#xff0c;影响整个环境的使用&#xf…

Meta 开启 Ray-Ban人工智能眼镜多模态 AI 功能测试,可识别物体、翻译语言

Meta 公司宣布他们将向部分用户推送其 Meta Ray-Ban 智能眼镜的多模态 AI 功能。这项功能允许 AI 助手通过眼镜的摄像头和麦克风了解佩戴者所看到和听到的事物&#xff0c;并提供相关信息和帮助。 Meta CEO 马克・扎克伯格在 Instagram 上展示了这项功能&#xff0c;他让眼镜推…

【WSL】Windows下的Linux子系统使用方法指南

▒ 目录 ▒ &#x1f6eb; 导读需求开发环境 1️⃣ WSL安装启用或关闭windows功能安装分发命令行启动Linux 2️⃣ WSL 的基本命令显示帮助列出可用的 Linux 发行版列出已安装的 Linux 发行版检查 WSL 版本更新 WSL通过 PowerShell 或 CMD 运行特定的 Linux 发行版关闭WSL全部服…

Amazon SageMaker:让机器学习变得更简单、更强大

授权说明&#xff1a;本篇文章授权活动官方亚马逊云科技文章转发、改写权&#xff0c;包括不限于在 亚马逊云科技开发者社区, 知乎&#xff0c;自媒体平台&#xff0c;第三方开发者媒体等亚马逊云科技官方渠道。 前言&#xff1a; 在大数据时代的浪潮中&#xff0c;数据不再只…

期末数组函数加强练习

前言&#xff1a;由于时间问题&#xff0c;部分题解取自网友&#xff0c;但都是做过的好题。 对于有些用c实现的题目&#xff0c;可以转化成c实现&#xff0c;cin看成c的读入&#xff0c;可以用scanf&#xff0c;输出cout看作printf&#xff0c;endl即换行符 开胃菜&#xff…

多项创新技术加持,汉威科技危化品企业、化工园区两大智能化管控平台重磅发布

化工产业是我国支柱产业&#xff0c;化学品产值约占全球的40%&#xff0c;位列第一。石油化工、煤化工、化肥、制药、冶金、氢能源等产业持续升级迭代&#xff0c;化工园区作为重要载体&#xff0c;不断推动产业朝专业化、集约化方向发展。 近年来我国危险化学品领域重特大事故…

动手学深度学习-注意力机制

10.1注意力提示 自主性注意力机制 有意识的注意力机制。非自主性注意力机制 无意识的注意力机制。 小结: 人类的注意力是有限的&#xff0c;有价值和稀缺的资源。受试者使用非自主性和自主性提示有选择的引导注意力&#xff0c;前者基于突出性&#xff0c;后者则依赖于意识。…

【Spring教程28】Spring框架实战:从零开始学习SpringMVC 之 请求与请求参数详解

目录 1 设置请求映射路径1.1 环境准备 1.2 问题分析1.3 设置映射路径 2 请求参数2.1 环境准备2.2 参数传递2.2.1 GET发送单个参数2.2.2 GET发送多个参数2.2.3 GET请求中文乱码2.2.4 POST发送参数2.2.5 POST请求中文乱码 欢迎大家回到《Java教程之Spring30天快速入门》&#xff…

【操作系统导论】内存篇——分段

引入 利用基址和界限寄存器&#xff0c;操作系统很容易将不同进程重定位到不同的物理内存区域。 但是&#xff0c;对于一整个地址空间&#xff0c;在栈和堆之间存在一块空闲空间&#xff0c;如果将整个地址空间分配给进程&#xff0c;无疑造成了大量的内存浪费。 为了解决这…

软件测试面试八股文(答案解析+视频教程)

1、B/S架构和C/S架构区别 B/S 只需要有操作系统和浏览器就行&#xff0c;可以实现跨平台&#xff0c;客户端零维护&#xff0c;维护成本低&#xff0c;但是个性化能力低&#xff0c;响应速度较慢。 C/S响应速度快&#xff0c;安全性强&#xff0c;一般应用于局域网中&#xf…

场景化文案怎么来的?媒介盒子拆解写作技巧

为什么有时候你的软文营销起不到作用、用户不买单&#xff1f;可能是引起你没有做好场景提醒&#xff0c;没有切中用户的切身利益点。今天媒介盒子就来和大家聊聊&#xff1a;场景化文案怎么写。 一、 场景文案核心点 在了解场景化文案的写作技巧前&#xff0c;咱们先来理清场…

如何将3dMax重置为默认的“出厂”设置?

如何将3dMax重置为“出厂”设置&#xff1f; 我们在使用3dMax软件时&#xff0c;有时会遇到一些问题&#xff0c;比如&#xff1a;启动时或使用程序内的特定功能时崩溃&#xff1b;3dMax界面显示问题&#xff1b;视口操纵缓慢或不稳定&#xff1b;键盘快捷方式丢失&#xff1b;…

Re9 Attention is all you need

变形金刚&#xff0c;启动&#xff01; Abstract 主流序列转录模型基于复杂的循环神经网络和卷积神经网络&#xff0c;包括一个encoder和decoder&#xff0c;同时在这之中使用一个叫注意力机制attention的东西本文提出了一个简单的网络架构&#xff0c;仅仅使用注意力机制&am…

0基础学java-day19(IO流)

一、文件 1 什么是文件 2.文件流 3.常用的文件操作 3.1 创建文件对象相关构造器和方法 package com.hspedu.file;import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test;import java.io.File; import java.io.IOException;/*** author 林然* vers…

多模态融合slam技术学习

目录 前言 一、视觉-惯性SLAM算法 二、激光-惯性SLAM算法 三、激光与视觉融合算法 总结 前言 最近在找实习&#xff0c;又想着要怎么完成毕业论文&#xff0c;打算做下机器人导航实验&#xff0c;学习下相关算法。今天是多模态融合slam技术&#xff0c;课程链接&#xff1a;h…

严世芸龟法养生经

文章目录 严世芸理念荤素搭配&#xff0c;不偏嗜动静结合心平气和 龟息法 严世芸 严世芸&#xff0c;出生于1940年&#xff0c;现任上海中医药大学的主任医师&#xff0c;教授。他父亲是近代上海有名的中医&#xff0c;他又是著名医家张伯臾的亲传弟子。 从小就在父亲诊室里长…

ArkTS入门

代码结构分析 struct Index{ } 「自定义组件&#xff1a;可复用的UI单元」 xxx 「装饰器&#xff1a;用来装饰类结构、方法、变量」 Entry 标记当前组件是入口组件&#xff08;该组件可被独立访问&#xff0c;通俗来讲&#xff1a;它自己就是一个页面&#xff09;Component 用…