Java关键字volatile详析
作者:负债程序猿? 发布时间:2023-01-21 21:51:11
volatile关键字关于先说它的两个作用:
保证变量在内存中对线程的可见性
禁用指令重排
每个字都认识,凑在一起就麻了
这两个作用通常很不容易被我们Java开发人员正确、完整地理解,以至于许多同学不能正确地使用volatile
一、可见性
码:
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
:执行+1PUTSTATIC
:写入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是如何实现禁用指令重排的:
这是个单例模式的实现,下面是它的部分字节码,红框中 mov%eax,0x150(%esi) 是对instance赋值
可以看到,在赋值后,还执行了 lock addl$0x0,(%esp) 指令,关键点就在这儿,这行指令相当于此处设置了个 内存屏障 ,有了内存屏障后,cpu或虚拟机在指令重排时就不能把内存屏障后面的指令提前到内存屏障前面,好好捋一下这段话
来源:https://blog.csdn.net/qq_33709582/article/details/122415754


猜你喜欢
- using System;using System.Collections.Generic;using System.Net;using S
- 本来项目是用的viewpager实现的轮播滚动,但是客户觉得轮播的效果太大众化了,于是就要我们改成渐变切换的效果。听到这需求,我最先想到是给
- FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化
- 思路:先从集合中找出来顶级的菜单,然后遍历顶级菜单,找出每个顶级菜单的所有子菜单,然后判断当前需要排列的集合是否为空,如果不为空的话,就在遍
- 前言:本文源码基于spring-framework-5.3.10。mvc是spring源码中的一个子模块!一、RequestMappingH
- Android中的ListView应该算是布局中几种最常用的组件之一了,使用也十分方便,下面将介绍ListView几种比较常见的优化方法:首
- @GetMapping和@GetMapping(value=““)区别背景初期对于@GetMappi
- Android 使用AsyncTask设置请求超时的注意事项final AsyncTaskTools task = new AsyncTas
- 一、事件背景个人感觉自己做性能测试,可以说是轻车熟路了,而且工作多年一直都是这一套测试思路及体系,从未质疑过自己,也许是狮子座的迷之自信吧!
- 前言如今多线程编程已成为了现代软件开发中的重要部分,而并发编程中的线程同步问题更是一道难以逾越的坎。在Java语言中,synchronize
- 做了微信支付,下载了Demo,发现和之前有所改动,v3.0的版本,也许有的朋友还在摸索,这里我已经成功支付,话不多说,直接进入主题:一、首先
- 概要应同学邀请,演示如何使用 PyQt5 内嵌浏览器浏览网页,并注入 Javascript 脚本实现自动化操作。下面测试的是一个廉价机票预订
- Java goto语句妙用今天和朋友聊天的时候,无意间聊到了 goto 语句,但是在 Java 中, goto 是保留关键字,但是朋友说 J
- 一、新时间日期API常用、重要对象介绍ZoneId: 时区ID,用来确定Instant和LocalDateTime互相转换的规则Instan
- 我们知道,(1)如果是整百的年份,能被400整除的,是闰年;(2)如果不是整百的年份,能被4整除的,也是闰年。每400年,有97个闰年。鉴于
- 本文实例讲述了Java实现的3des加密解密工具类。分享给大家供大家参考,具体如下:package com.gcloud.common;im
- 一、题目描述二、思路语法基础:StringBuilder 类似列表,可以更改元素。package Practice;public class
- 目录一、准备工作1、导包二、了解注解1、常用注解2、@ExcelProperty注解3、@ColumnWith注解4、@ContentFon
- 本文实例为大家分享了Unity实现俄罗斯方块游戏的具体代码,供大家参考,具体内容如下一、演示二、实现思路创建每一个方块可移动到的位置点,可以
- 一、基本概念1、进程首先打开任务管理器,查看当前运行的进程:从任务管理器里面可以看到当前所有正在运行的进程。那么究竟什么是进程呢?进程(Pr