java并发编程关键字volatile保证可见性不保证原子性详解

 更新时间:2022年02月08日 14:49:14   作者:负债程序猿  
这篇文章主要为大家介绍了java并发编程关键字volatile保证可见性不保证原子性详解,文中附含详细示例说明,有需要的朋友可以借鉴参考下,希望能够有所帮助

volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制,但对于为什么它只能保证可见性,不保证原子性,它又是如何禁用指令重排的,还有很多同学没彻底理解

相信我,坚持看完这篇文章,你将牢牢掌握一个Java核心知识点

先说它的两个作用:

保证变量在内存中对线程的可见性禁用指令重排

每个字都认识,凑在一起就麻了

这两个作用通常很不容易被我们Java开发人员正确、完整地理解,以至于许多同学不能正确地使用volatile

关于可见性

不多bb,码来

public class VolatileTest {
    private static volatile int count = 0;
    private static void increase() {
        count++;
    }
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    increase();
                }
            }).start();
        }
		// 所有线程累加完成后输出
        while (Thread.activeCount() > 2) Thread.yield();
        System.out.println(count);
    }
}

代码很好理解,开了十个线程对同一个共享变量count做累加,每个线程累加1w次

count我们已经用volatile修饰,已经保证了count对十个线程在内存中的可见性,按理说十个线程执行完毕count的值应该10w

然鹅,运行多次,结果都远小于期望值

在这里插入图片描述

是哪个环节出了问题?

在这里插入图片描述

你肯定听过一句话:volatile只保证可见性,不保证原子性

这句话就是答案,但是依旧很多人没搞懂其中的奥秘

说来话长我长话短说,简单来讲就是 count++这个操作不是原子的,它是分三步进行

  • 从内存读取 count 的值
  • 执行 count + 1
  • 将 count 的新值写回

要彻底搞懂这个问题,我们得从字节码入手

下面是increase方法编译后的字节码

在这里插入图片描述

看不懂没关系,我们一行一行来看:

  • GETSTATIC:读取 count 的当前值
  • ICONST_1:将常量 1 加载到栈顶
  • IADD:执行+1
  • PUTSTATIC:写入count最新值

ICONST_1和IADD其实就是真正的++操作

关键点来了,volatile只能保证线程在GETSTATIC这一步拿到的值是最新的,但当该线程执行到下面几行指令时,这期间可能就有其它线程把count的值修改了,最终导致旧值把真正的新值覆盖

懂我意思吗

所以,并发编程中,只靠volatile修饰共享变量是不可靠的,最终还是要通过对关键方法加锁来保证线程安全

就如上面的demo,稍加修改就能实现真正的线程安全

最简单的,给increase方法加个synchronized (synchronized怎么实现线程安全的我就不啰嗦了,我以前讲过 synchronized底层实现原理

    private synchronized static void increase() {
        ++count;
    }

run几下

在这里插入图片描述

这不就妥了嘛

到现在,对于以下两点你应该有了新的认知

volatile保证变量在内存中对线程的可见性

volatile只保证可见性,不保证原子性

关于指令重排

并发编程中,cpu自身和虚拟机为了提高执行效率,都会采用指令重排(在保证不影响结果的前提下,将某些代码乱序执行)

  • 关于cpu:为了从分利用cpu,实际执行指令时会做优化;
  • 关于虚拟机:在HotSpot vm中,为了提升执行效率,JIT(即时编译)模式也会做指令优化

指令重排在大部分场景下确实能提升执行效率,但有些场景对代码执行顺序是强依赖的,此时我们需要禁用指令重排,如下面这个场景

在这里插入图片描述

伪代码取自《深入理解Java虚拟机》:

其描述的场景是开发中常见配置读取过程,只是我们在处理配置文件时一般不会出现并发,所以没有察觉这会有问题。
试想一下,如果定义initialized变量时没有使用volatile修饰,就可能会由于指令重排序的优化,导致位于线程A中最后一条代码“initialized=true”被提前执行(这里虽然使用Java作为伪代码,但所指的重排序优化是机器级的优化操作,提前执行是指这条语句对应的汇编代码被提前执行),这样在线程B中使用配置信息的代码就可能出现错误,而volatile通过禁止指令重排则可以避免此类情况发生

禁用指令重排只需要将变量声明为volatile,是不是很神奇

我们来看看volatile是如何实现禁用指令重排的

也借用《深入理解Java虚拟机》的一个例子吧,比较好理解

在这里插入图片描述

这是个单例模式的实现,下面是它的部分字节码,红框中 mov%eax,0x150(%esi) 是对instance赋值

在这里插入图片描述

可以看到,在赋值后,还执行了 lock addl$0x0,(%esp) 指令,关键点就在这儿,这行指令相当于此处设置了个 内存屏障 ,有了内存屏障后,cpu或虚拟机在指令重排时就不能把内存屏障后面的指令提前到内存屏障前面,好好捋一下这段话

最后,留一个能加深大家对volatile理解的问题,兄弟们好好思考下:

Java代码明明是从上往下依次执行,为什么会出现指令重排这个问题?

以上就是java并发编程关键字volatile保证可见性不保证原子性详解的详细内容,更多关于java并发编程关键字volatile的资料请关注脚本之家其它相关文章!

相关文章

  • 使用Spring事物时不生效的场景及解决方法

    使用Spring事物时不生效的场景及解决方法

    今天介绍一下Spring事物不生效的场景,事物是我们在项目中经常使用的,如果是Java的话,基本上都使用Spring的事物,不过Spring的事物如果使用不当,那么就会导致事物失效或者不回滚,最终导致数据不一致,下面我们意义列举不生效的场景,并给出解决方法
    2023-09-09
  • Java实现FIFO、LRU、LFU、OPT页面置换算法

    Java实现FIFO、LRU、LFU、OPT页面置换算法

    本文主要介绍了Java实现FIFO、LRU、LFU、OPT页面置换算法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-02-02
  • 利用Java编写一个出敬业福的小程序

    利用Java编写一个出敬业福的小程序

    新年将至,又开始扫福活动,每年的敬业福成了大家难过的坎。所以本文将介绍一个通过Java编写的一款福字生成器,感兴趣的小伙伴可以试一试
    2022-01-01
  • Mybatis使用MySQL模糊查询时输入中文检索不到结果怎么办

    Mybatis使用MySQL模糊查询时输入中文检索不到结果怎么办

    这篇文章主要介绍了Mybatis使用MySQL模糊查询时输入中文检索不到结果的解决办法的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2016-07-07
  • Tomcat 服务器 在45秒内未启动成功的解决方法

    Tomcat 服务器 在45秒内未启动成功的解决方法

    下面小编就为大家带来一篇Tomcat 服务器 在45秒内未启动成功的解决方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-11-11
  • 简单谈谈RxJava和多线程并发

    简单谈谈RxJava和多线程并发

    认识RxJava已经有一段时间了,但是一直没有机会在项目中尝试,最近在新的项目里引进了RxJava写一些事件处理,在review代码的时候发现了一些和多线程并发相关的问题,所以写了这篇文章,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-03-03
  • Java String 对象(你真的了解了吗)

    Java String 对象(你真的了解了吗)

    这篇文章主要介绍了Java String 对象(你真的了解了吗),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-10-10
  • 详解java WebSocket的实现以及Spring WebSocket

    详解java WebSocket的实现以及Spring WebSocket

    这篇文章主要介绍了详解java WebSocket的实现以及Spring WebSocket ,具有一定的参考价值,感兴趣的小伙伴们可以参考一下。
    2017-01-01
  • Mybatis-Plus的saveOrUpdateBatch(null)问题及解决

    Mybatis-Plus的saveOrUpdateBatch(null)问题及解决

    这篇文章主要介绍了Mybatis-Plus的saveOrUpdateBatch(null)问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-07-07
  • Java8新特性之Stream使用详解

    Java8新特性之Stream使用详解

    这篇文章主要介绍了Java8新特性之Stream使用详解,流是用来处理集合中的数据,以声明的形式操作集合,它就像SQL语句,我们只需告诉流需要对集合进行什么操作,它就会自动进行操作,并将执行结果交给你,无需我们自己手写代码,需要的朋友可以参考下
    2023-08-08

最新评论