volatile指令重排_有多少人面试栽到Volatile上?面试问题都总结到这儿了

Volatile关键字

volatile 是Java虚拟机提供的 轻量级 的同步机制.何为 轻量级 呢,这要相对于 synchronized 来说。Volatile有如下三个特点。

要搞清楚上面列举的名词 可见性 原子性 指令重排 的含义我们需要首先弄清楚JMM(Java内存模型是怎么回事)

JMM规定了内存主要划分为 主内存 和 工作内存 两种。此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存.

e525f7837536a6327518db463bbf5c24.png

JVM在设计时候考虑到,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份 拷贝 ,线程对变量的读取和写入,直接在工作内存中操作,而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。因为JMM制定了一套标准来保证开发者在编写多线程程序的时候,能够控制什么时候内存会被同步给其他线程。

各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回主内存中的。

这就可能存在一个线程A修改了共享变量X的值但还未写回主内存时,另一个线程B又对准内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量X对线程B来说并不是可见,这种工作内存与主内存同步存在延迟现象就造成了可见性问题。

通过代码来看下可见性的问题

package com.dpb.spring.aop.demo;import java.util.concurrent.TimeUnit;/** * 可见性问题分析 */public class VolatileDemo1 {    public static void main(String[] args){        final MyData myData = new MyData();        // 开启一个新的线程        new Thread(()->{            System.out.println(Thread.currentThread().getName() + "开始了...");            try{TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}            // 在子线程中修改了变量的信息  修改的本线程在工作内存中的数据            myData.addTo60();            System.out.println(Thread.currentThread().getName() + "更新后的数据是:"+myData.number);        },"BBB").start();        // 因为在其他线程中修改的信息主线程的工作内存中的数据并没有改变所以此时number还是为0        while(myData.number == 0){            // 会一直卡在此处            //System.out.println("1111");        }        System.out.println(Thread.currentThread().getName()+" number =  " + myData.number);    }}class MyData{// 没有用volatile来修饰    int number = 0;    public void addTo60(){        this.number = 60;    }}

效果如下:

736b682faf5d0463c6d6e9c10dfbf44b.png

通过 volatile 来解决此问题

32b0df9d83917a6f9bb28bc722dbb116.png
5605a338bd345d0655035e47299dd86f.png

我们可以发现当变量被 volatile 修饰的时候,在子线程的工作内存中的变量被修改后其他线程中对应的变量是可以立马知道的。这就是我们讲的可见性

原子性是 不可分割 , 完整性 ,也即某个线程正在做某个具体业务时,中间不可以被加塞或者分割,需要整体完成,要么同时成功,要么同时失败.

volatile是 不支持 原子性的,接下来我们可以验证下。

package com.dpb.spring.aop.demo;import java.util.concurrent.TimeUnit;/** * 可见性问题分析 */public class VolatileDemo2 {    public static void main(String[] args){        final MyData2 myData = new MyData2();        for (int i = 1; i <= 20 ; i++) {            new Thread(()->{                for (int j = 1; j <= 1000 ; j++) {                    myData.addPlusPlus();                }            },String.valueOf(i)).start();        }        // 等待子线程执行完成        while(Thread.activeCount() > 2){            Thread.yield();        }        // 在主线程中获取统计的信息值        System.out.println(Thread.currentThread().getName()+" finnally number value: "+myData.number);    }}class MyData2{   // 操作的变量被volatile修饰了    volatile int number = 0;    public void addPlusPlus(){        number++;    }}

执行的效果

d62bfb39033f4d41c668817bc9bed7e8.gif

根据正常的逻辑在开启的20个子线程,每个执行1000遍累加,得到的结果应该是20000,但是我们发现运行的结果大概率会比我们期望的要小,而且变量也已经被volatile修饰了。说明并没有满足我们要求的原子性。这种情况下我们要保证操作的原子性,我们有两个选择

  1. 通过synchronized来实现
  2. 通过 JUC 下的 AtomicInteger 来实现

synchronized的实现是重量级的,影响并发的效率,所以我们通过AtomicInteger来实现。

package com.dpb.spring.aop.demo;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicInteger;/** * 可见性问题分析 */public class VolatileDemo2 {    public static void main(String[] args){        final MyData2 myData = new MyData2();        for (int i = 1; i <= 20 ; i++) {            new Thread(()->{                for (int j = 1; j <= 1000 ; j++) {                    myData.addPlusPlus();                    myData.addAtomicPlus();                }            },String.valueOf(i)).start();        }        // 等待子线程执行完成        while(Thread.activeCount() > 2){            Thread.yield();        }        // 在主线程中获取统计的信息值        System.out.println(Thread.currentThread().getName()+" finnally number value: "+myData.number);        System.out.println(Thread.currentThread().getName()+" finnally number value: "+myData.atomicInteger.get());    }}class MyData2{   // 操作的变量被volatile修饰了    volatile int number = 0;    // AtomicInteger 来保证操作的原子性    AtomicInteger atomicInteger = new AtomicInteger();    public  void addPlusPlus(){        number++;    }    public void addAtomicPlus(){        atomicInteger.getAndIncrement();    }}

效果:

d120d6c98279d098f9cc3ec0933fe182.gif

注意 :通过效果发现 AtomicInteger 在多线程环境下处理的数据和我们期望的结果是一致的都是 20000 .说明实现的操作的原子性。

计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分以下3种:

5dd3624036461b7c6256ce895658fb99.png
数据依赖性

案例代码

package com.dpb.spring.aop.demo;public class SortDemo {    int a = 0;    boolean flag = false;    public void fun1(){        a = 1;  // 语句1        flag = true; // 语句2    }    public void fun2(){        if(flag){            a = a + 5; // 语句3            System.out.println("a = " + a );        }    }}

注意: 在多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

指令重排小结:

volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。

先了解一个概念, 内存屏障 又称 内存栅栏 ,是一个CPU指令,它的作用有两个:

  1. 是保证特定操作的执行顺序
  2. 是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重新排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

线程安全的总结:

  1. 工作内存和主内存同步延迟现象导致的 可见性问题 ,可以使用synchronized或volatile关键字解决,他们都可以使一个线程修改后的变量立即对其他线程可见。
  2. 对于指令重排导致的 可见性问题 和 有序性问题 ,可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。

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

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

相关文章

Dubbo核心概念

节点角色规范 节点角色规格Provider提供者公开远程服务Consumer消费者致电远程服务Registry注册表负责服务发现和配置Monitor监视器计算服务调用的数量和耗时Container容器管理服务的生命周期 服务关系 Container负责启动&#xff0c;加载和运行服务Provider。ProviderRegiste…

良心推荐11款可以称得上“神器”的Windows工具集合

1、最快文件搜索工具 Everything&#xff1a;当之无愧的最强本地文件搜索神器&#xff0c;搜索任何关键词基本是秒速出现&#xff0c;比Windows自带的搜索快了太多&#xff0c;电脑文件比较多的人必备&#xff01; 2、专业软件卸载器 Revo Uninstaller Pro&#xff1a;Windows电…

LD3320语音识别模块二次开发及与树莓派间的通讯

实物图如下&#xff1a; 一般这种模块的资料厂家都会给&#xff0c;需要的话可以私信我发邮箱&#xff0c;下面介绍该模块的各种参数。型号&#xff1a;YS-LDV7名称&#xff1a;一体化语音识别模块规格&#xff1a;43*29.7MM供电电压&#xff1a;5V &#xff08;内部工作电压…

多生产者_你是生产者还是消费者?这决定了你的层次。

不知道你有没有注意到&#xff0c;每天乘坐地铁上下班的时候&#xff0c;大部分人都在刷剧、看视频、打游戏等等&#xff0c;总之都属于玩乐。用生产和消费的关系来看的话&#xff0c;其实这一大部分人都属于消费者&#xff0c;“时间和注意力”是他们用于交换的筹码&#xff1…

eclipse Android 开发基础 Activity 窗体 界面

eclipse Android 开发基础 新建工程 新建布局layout,new Android Activity就相当于窗体Form。 新建Activity自动生成src下同名的java代码。 public class Tform2activity extends Activity {Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(saved…

8 种常被忽视的 SQL 错误用法

来源&#xff1a;http://t.cn/R6UMaA11、LIMIT 语句2、隐式转换3、关联更新、删除4、混合排序5、EXISTS语句6、条件下推7、提前缩小范围8、中间结果集下推总结sql语句的执行顺序&#xff1a;FROM <left_table>ON <join_condition><join_type> JOIN <right…

变频器按启动没反应_起重机软启动柜晶闸管损坏维修几大故障

缺相保护功能&#xff1a;工作时&#xff0c;软起动器随时检测三相线电流的变化&#xff0c;一旦发生断流&#xff0c;即可作出缺相保护反应。过热保护功能&#xff1a;通过软起动器内部热继电器检测晶闸管散热器的温度&#xff0c;一旦散热器温度超过允许值后自动关断晶闸管&a…

Redis 的各项功能解决了哪些问题?

作者丨blackheart先看一下Redis是一个什么东西官方简介解释到&#xff1a;Redis是一个基于BSD开源的项目&#xff0c;是一个把结构化的数据放在内存中的一个存储系统&#xff0c;你可以把它作为数据库&#xff0c;缓存和消息中间件来使用。同时支持strings&#xff0c;lists&am…

RocketMQ集成SpringBoot

RocketMQ集成SpringBoot RocketMQ总体架构 RocketMQ基本特性

ASP.NET Core 2.2+Quartz.Net 实现Web定时任务

作者&#xff1a;Julian_酱链接&#xff1a;http://www.cnblogs.com/mi12205599/p/10361763.html作为一枚后端程序狗&#xff0c;项目实践常遇到定时任务的工作&#xff0c;最容易想到的的思路就是利用Windows计划任务/wndows service程序/Crontab程序等主机方法在主机上部署定…

RocketMQ核心概念

生产者Producer和消费者Consumer NameServer作用 Broker和Topic

交叉编译、软硬链接

什么是交叉编译&#xff1f;交叉编译是一个行为&#xff0c;是在一个平台上生成另一个平台上的可执行代码。 本地编译&#xff1a;本地编译可以理解为&#xff0c;在当前编译平台下&#xff0c;编译出来的程序只能放到当前平台下运行。平时我们常见的软件开发&#xff0c;都是…

Linus下安装maven

下载maven安装包 wget http://mirror.bit.edu.cn/apache/maven/binaries/apache-maven-3.2.2-bin.tar.gz 解压 tar -zxvf apache-maven-3.2.2-bin.tar.gz 配置maven环境变量 查看maven解压后安装包目录 vi /etc/profile 进入最底部&#xff0c;按insert,添加环境变量&#x…

linux内核开发基础(linux内核源码、树莓派源码编译、SD卡挂载)

首先下载树莓派linux内核源码&#xff1a; 下载网址&#xff1a;https://github.com/raspberrypi/linux在树莓派使用指令&#xff1a;uname -r查看当前树莓派的版本号&#xff0c;然后选择对应的linux内核版本号进行下载。 将linux内核源码从共享文件夹拷贝到SYSTEM文件夹&am…

Linux实时查看进程命令top笔记

top命令是Linux下常用的性能分析工具&#xff0c;能够实时显示Linux系统中各个进程的资源占用状况&#xff0c;类似于Windows系统的任务管理器功能。 top命令的语法格式&#xff1a; top [-] [d] [p] [q] [c] [C] [S] [s] [n] 常用参数说明 d 指定每两次屏幕信息刷新之间的时间…

文件系统(文件系统目录结构、磁盘分区、虚拟文件系统)、linux内核结构框图

什么是文件系统&#xff1f; 常规认知就是根目录下那些文件&#xff0c;但其实并不是那样。文件系统是操作系统用于明确存储设备&#xff08;常见的是磁盘&#xff0c;也有基于NAND Flash的固态硬盘&#xff09;或分区上的文件的方法和数据结构&#xff1b;即在存储设备上组织…

Linux进程终止命令kill或kill all​笔记

在linux命令下&#xff0c;如果需要终止某个进程&#xff0c;可以使用kill或者killall等命令来实现。终止命令的原理都是向linux内核发送一个系统操作的信号以及某个进程的ID&#xff0c;然后系统内核会根据指定的进程ID进行相应的处理。 kill命令典型的用法&#xff1a;首先使…

linux驱动(驱动编译、字符设备驱动框架、交叉编译树莓派驱动、树莓派驱动本地编译)

什么是驱动&#xff1a; 驱动就是对底层硬件设备的操作进行封装&#xff0c;并向上层提供函数接口。 设备分类&#xff1a; linux系统将设备分为3类&#xff1a;字符设备、块设备、网络设备。 字符设备&#xff1a;指只能一个字节一个字节读写的设备&#xff0c;不能随机读取…

docker启动报错  (iptables failed: iptables --wait -t nat -A DOCKER -p tcp -d 0/0 --dport 9876 -j DNAT --

docker启动报错 : (iptables failed: iptables --wait -t nat -A DOCKER -p tcp -d 0/0 --dport 9876 -j DNAT --to-destination 172.17.0.2:9876 ! -i docker0: iptables: No chain/target/match by that name. 解决方案&#xff1a; systemctl restart docker