当前位置:

首页 > 编程开发 > Java中Volatile关键字能不能保证原子性

Java中Volatile关键字能不能保证原子性

volatilevolatile是Java中的一个相对来说比较重要的关键字,主要就是用来修饰会被不同线程访问和修改的变量。而这个变量只能保证两个特性,一个是保证有序性,另外一个则是保证可见性。那么什么是有序性,什么又是可见性呢?有序性那么什么是有序性呢?其实程序执行的顺序按照代码的先后顺序执行,禁止进行指令重排序。看似理所当然,其实并不是这样,指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。但是在多线程环境下,有些代码的顺序改变,有可能引发逻辑上的

本文目录

    volatile

    volatile 是 Java 中的一个相对来说比较重要的关键字,主要就是用来修饰会被不同线程访问和修改的变量。

    而这个变量只能保证两个特性,一个是保证有序性,另外一个则是保证可见性。

    那么什么是有序性,什么又是可见性呢?

    有序性

    那么什么是有序性呢?

    其实程序执行的顺序按照代码的先后顺序执行,禁止进行指令重排序。

    看似理所当然,其实并不是这样,指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。

    但是在多线程环境下,有些代码的顺序改变,有可能引发逻辑上的不正确。

    而 volatile 就是因为有这个特性,所以才被大家熟知的。

    volatile 又是如何保证有序性的呢?

    有很多小伙伴就说,网上说的是 volatile 可以禁止指令指令重排序,这就保证了代码的程序会严格按照代码的先后顺序执行。这就保证了有序性。被 volatile 修饰的变量的操作,会严格按照代码顺序执行,就是说当代码执行到 volatile 修饰的变量时,其前面的代码一定执行完毕,后面的代码一定没有执行。

    如果这时候,面试官不再继续深挖下去的话,那么恭喜你,可能这个问题已经回答完了,但是如果面试官继续往下深挖,为什么会禁止指令重排,什么又是指令重排呢?

    在从源码到指令的执行,一般是分成了三种重排,如图所示:                             

    Java中Volatile关键字能不能保证原子性

    我们接下来就得看看 volatile 是如何禁止指令重排的。

    我们直接用代码来进行验证:

    public class ReSortDemo {
    
    int a = 0;
    boolean flag = false;
    
    public void mehtod1(){
    a = 1;
    flag = true;
    }
    
    public void method2(){
    if(flag){
    a = a +1;
    System.out.println("最后的值: "+a);
    }
    }
    }

    如果有人看到这段代码,肯定会说,那这段代码出来的结果会是什么呢?

    有些人说是 2,是的, 如果你只是单线程调用,那结果就是 2,但是如果是多线程调用的时候,最后的输出结果不一定是我们想象到的 2,这时就要把两个变量都设置为 volatile。

    如果大家对单例模式了解比较多的话,肯定也是关注过这个 volatile,为什么呢?

    大家看看如下代码:

    class Singleton {
    // 不是一个原子性操作
    //private static Singleton instance;
    //改进,Volatile 可以保持可见性,不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!
    private static volatile Singleton instance;
    
    // 构造器私有化
    private Singleton() {
    }
    
    // 提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题,同时保证了效率, 推荐使用
    public static Singleton getInstance() {
    if (instance == null) {
    synchronized (Singleton.class) {
    if (instance == null) {
    instance = new Singleton();
    }
    }
    }
    return instance;
    }
    }

    上面的单例模式大家熟悉么?

    是的,这就是 **双重检查(DCL 懒汉式) **

    有人会说,因为有指令重排序的存在,双端检索机制也也不一定是线程安全的呀,对呀,所以用到了 synchronized 关键字,让他变成了线程安全的了。

    可见性

    其实可见性就是,在多线程环境中,对共享变量的修改对于其他线程是否立即可见的问题。

    那么他的可见性一般都会表现在什么地方呢?用在什么地方呢?

    其实一般用这个变量,很多都是为了保证他的可见性,就比如定义的一个全局变量,在其中有个循环来判断这个变量的值,有一个线程修改了这个参数的时候,这个循环会停止,跳转到之后去执行。

    我们来看看没有使用volatile修饰代码实现:

    public class Test {
    
    private static boolean flag = false;
    
    public static void main(String[] args) throws Exception{
    new Thread(new Runnable() {
    @Override
    public void run() {
    System.out.println("线程A开始执行:");
    for (;;){
    if (flag){
    System.out.println("跳出循环");
    break;
    }
    }
    }
    }).start();
    Thread.sleep(100);
    
    new Thread(new Runnable() {
    @Override
    public void run() {
    System.out.println("线程B开始执行");
    flag = true;
    System.out.println("标识已经变更");
    }
    }).start();
    }
    }

    结果大家肯定是可想而知,

    运行结果肯定是:

    线程A开始执行:
    线程B开始执行
    标识已经变更

    确实,就是这样的。

    Java中Volatile关键字能不能保证原子性

    如果我们用 volatile 呢,那么这个代码的执行结果就会不一样呢?

    我们来试一下:

    public class Test {
    
    private static volatile boolean flag = false;
    
    public static void main(String[] args) throws Exception{
    new Thread(new Runnable() {
    @Override
    public void run() {
    System.out.println("线程A开始执行:");
    for (;;){
    if (flag){
    System.out.println("跳出循环");
    break;
    }
    }
    }
    }).start();
    Thread.sleep(100);
    
    new Thread(new Runnable() {
    @Override
    public void run() {
    System.out.println("线程B开始执行");
    flag = true;
    System.out.println("标识已经变更");
    }
    }).start();
    }

    这样我们就能看到另外一个执行结果,在循环当中的输出语句是可以被执行的。

    Java中Volatile关键字能不能保证原子性

    也就是说,在线程B 中,我们去修改这个被修饰的变量,那么最终,在线程A中,就能顺利读取到我们的数据信息了。

    是否能够保证原子性

    不能,我们来看一点代码,被volatile修饰的变量,

    public class Test {
    
    // volatile不保证原子性
    // 原子性:保证数据一致性、完整性
    volatile int number = 0;
    
    public void addPlusPlus() {
    number++;
    }
    
    public static void main(String[] args) {
    Test volatileAtomDemo = new Test();
    for (int j = 0; j < 20; j++) {
    new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
    volatileAtomDemo.addPlusPlus();
    }
    }, String.valueOf(j)).start();
    }// 后台默认两个线程:一个是main线程,一个是gc线程
    while (Thread.activeCount() > 2) {
    Thread.yield();
    }
    // 如果volatile保证原子性的话,最终的结果应该是20000 // 但是每次程序执行结果都不等于20000
    System.out.println(Thread.currentThread().getName() +
    " final number result = " + volatileAtomDemo.number);
    }
    }

    如果能够保原子性,那么最终的结果应该是20000,但是每次的最终结果并不能保证就是20000,比如:

    main final number result = 17114
    main final number result = 20000
    main final number result = 19317

    三次执行,都是不同的结果,

    为什么会出现这种呢?这就和number++有关系了

    number++被拆分成3个指令

    • 执行GETFIELD拿到主内存中的原始值number

    • 执行IADD进行加1操作

    • 执行PUTFIELD把工作内存中的值写回主内存中

    当多个线程并发执行PUTFIELD指令的时候,会出现写回主内存覆盖问题,所以才会导致最终结果不为 20000,所以 volatile 不能保证原子性。

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Java volatile
    相关文章 更多
    Java单元测试入门:使用JUnit 5编写第一个测试用例
    Java单元测试入门:使用JUnit 5编写第一个测试用例

    面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

    Java变量未初始化报错解析:局部变量与成员变量默认值差异
    Java变量未初始化报错解析:局部变量与成员变量默认值差异

    深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

    详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

    Java中final与static final常量的定义及初始化规则
    Java中final与static final常量的定义及初始化规则

    深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

    Java抽象方法与接口方法的区别及选型建议
    Java抽象方法与接口方法的区别及选型建议

    深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

    Java实现HTTP请求与JSON解析的完整代码示例
    Java实现HTTP请求与JSON解析的完整代码示例

    本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

    Java递归出现StackOverflowError的原因分析及栈空间优化方案
    Java递归出现StackOverflowError的原因分析及栈空间优化方案

    详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

    Java空指针异常(NPE)的代码排查与修复实例
    Java空指针异常(NPE)的代码排查与修复实例

    针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

    selenium上传文件教程本地文件路径设置方法
    selenium上传文件教程本地文件路径设置方法

    深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

    针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程
    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版
    macOS

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版
    Windows

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。