volatile原理剖析和实例讲解

一、是什么

volatile是Java的一个关键字,是Java提供的一种轻量级的同步机制,

二、能做什么

保证了不同线程对这个变量进行操作时的可见性,有序性。

三、可见性

可见性主要是指一个线程修改了共享变量的值,另一个线程可以看见。但是每一个线程都是要自己的工作内存,那么要如何实现线程之间的可见内?使用volatile关键字就可以有效的解决可见性问题。下面用一个例子来解释一下线程可见性的问题。

public class VolatileDemo {static boolean flag = false;public static void main(String[] args) {//启用一个线程,如果主线程修改该布尔值为true则退出循环,如果线程中对于修改的值不可间,那么程序会一直在循环中new Thread(() -> {System.out.println(Thread.currentThread().getName() + "线程启动,falg为" + flag);while (!flag) {}System.out.println(Thread.currentThread().getName() + "退出循环");},"t1").start();try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);}//main线程把布尔值修改为trueflag = true;System.out.println(Thread.currentThread().getName()+"修改flag为" +flag);}
}

从上面的代码可以知道如果t1线程可以知道main线程的修改,那么t1线程中的for循环就可以正常退出,如果main线程的修改t1不可见,那么t1线程的循环就无法退出。如果我们在flag变量添加volatile关键字,如下所示。

public class VolatileDemo {//添加volatile关键字static volatile boolean flag = false;public static void main(String[] args) {//启用一个线程,如果主线程修改该布尔值为true则退出循环,如果线程中对于修改的值不可间,那么程序会一直在循环中new Thread(() -> {System.out.println(Thread.currentThread().getName() + "线程启动,falg为" + flag);while (!flag) {}System.out.println(Thread.currentThread().getName() + "退出循环");},"t1").start();try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);}//main线程把布尔值修改为trueflag = true;System.out.println(Thread.currentThread().getName()+"修改flag为" +flag);}
}

那么没有加volatile关键字线程t1中为何看不到被主线程main修改false的flag的值?

可能原因

  • 主线程修改了flag之后没有将其刷新到主内存所以t1线程看不到。
  • 主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中的flag的值,没有去主内存中更新获取flag最新的值。

使用volatile修饰共享变量有以下特点

  • 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存。
  • 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存。

四、有序性

指令重排

为了提高性能,在遵守 as-if-serial 语义(即不管怎么重排序,单线程下程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守。)的情况下,编译器和处理器常常会对指令做重排序。

一般重排序可以分为如下三种类型:

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

重排序的执行流程为执行流程
在这里插入图片描述

数据依赖性

若两个操作访问同一变量,且这两个操作中有一个为写操作,此两个操作间就存在数据依赖性。

下面用两个案列来说明什么是数据依赖性

public class volatileDemo01 {public static void main(String[] args) {int a = 1;int b = 2;int c = a + b;System.out.println(c);}
}
// 重排后的代码
public class volatileDemo01 {public static void main(String[] args) {int b = 2;int a = 1;int c = a + b;System.out.println(c);}
}

变量a和变量b调换位置,无论怎么调换都不会影响程序的最终结果所以就不存在数据依赖性。

名称代码示例说明
写后读a=1;b=a;写一个变量之后,再读这个位置
写后写a=1;a=2;写一个变量之后,再写这个变量
读后写a=b;b=1;读一个变量之后,再写这个变量

上面三种情况是存在数据依赖关系的,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。

内存屏障

为了实现 volatile 内存语义(即内存可见性),JMM 会限制特定类型的编译器和处理器重排序。为此,JMM 针对编译器制定了 volatile 重排序规则表,如下所示:

是否重排序第二次操作普通读/写第二次操作volatile读第二次操作volatile写
第一次操作普通读/写
第一次操作volatile读
第一次操作volatile写

上面表格的内容可以总结为以下3点

  • 当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile之前
  • 当第二个操作为volatile写是,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile之后
  • 当第一个操作为volatile写是,第二个操作为volatile读时,不能重排。

内存屏障是一组处理器指令,它的作用是禁止指令重排序和解决内存有序性的问题

JMM把内存屏障指令分为四类

  • 在每一个volatile写操作前面插入一个StoreStore屏障:StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已刷新到主内存。

  • 在每一个volatile写操作后面插入一个StoreLoad屏障:StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。

  • 在每一个volatile读操作后面插入一个LoadLoad屏障:LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。

  • 在每一个volatile读操作后面插入一个LoadStore屏障:LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。

class VolatileTest{int i=0;// 没有加volatile多线程的情况下会发生指令重排boolean flag = false;public void set(){i=2;flag=true;}public void get(){if (flag){System.out.println(i);}}
}

加上volatile该程序就变成线程安全的程序了,我们分析以下这个代码。

class VolatileTest{int i=0;// 没有加volatile多线程的情况下会发生指令重排boolean flag = false;public void set(){i=2;flag=true;}public void get(){if (flag){System.out.println(i);}}
}

在这里插入图片描述

左边是set方法的分析,右边是get方法的分析。因为给flag添加了volatile关键字,所以当对于flag的读写都会添加相应的屏障,在每一个volatile写操作后面都会插入一个StoreLoad屏障,volatile写不能与后面可能有的volatile读/写操作重排序volatile前面插入一个StoreStore屏障,可以保证在volatile写之前,其前面的所有普通写操作都已刷新到主内存。volatile读后面会添加LoadLoad屏障和LoadStore屏障。LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序

LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序

五、无原子性

下面我将用一个例子来说明volatile的无原子性

class Number {volatile int num = 0;public void add(){num++;}
}
public class volatileDemo01 {public static void main(String[] args) {Number number = new Number();for (int i = 0; i < 10; i++) {new Thread(()->{for (int j = 0; j < 1000; j++) {number.add();}}).start();}try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(number.num);}
}

上面这段代码如果是线程安全的话就会输出10000,但是由于volatile并不能保证原子性所以程序的输出结果每次基本上都不一样。

在这里插入图片描述

对于volatile变量具备可见性,JVM只是保证从主内存加载到线程工作内存值是最新的,也仅是数据加载时最新的。但是多线程环境下,“数据计算”和“数据赋值”操作可能多次出现,若数据在加载之后,若主内存volatile修饰变量发生修改之后,线程工作内存中的操作将会作废去读内存最新值,操作出现写丢失问题。各线程私有内存和主内存公共内存中变量不同步,进而导致数据不一致。由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改主内存共享变量的场景必须使用加锁同步

六、使用

volatile的运用

  • 当读远多于写,结合使用内部锁和volatile变量来减少同步的开销。

    public class UseVolatileDemo {private  volatile  int value;// 利用volatile保证读取操作的可见性public int getValue() {return value;}// 利用synchronized保证复合操作的原子性public synchronized  int incrementAndGet() {return value++;}
    }
    
  • 状态标志,判断业务是否结束。

    public class VolatileDemo {static volatile boolean flag = false;public static void main(String[] args) {//启用一个线程,如果主线程修改该布尔值为true则退出循环,如果线程中对于修改的值不可间,那么程序会一直在循环中new Thread(() -> {System.out.println(Thread.currentThread().getName() + "线程启动,falg为" + flag);while (!flag) {}System.out.println(Thread.currentThread().getName() + "退出循环");},"t1").start();try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);}//main线程把布尔值修改为trueflag = true;System.out.println(Thread.currentThread().getName()+"修改flag为" +flag);}
    }
    
  • DCL双端锁的发布。

    public class SafeDoubleCheckSingleton {private volatile static SafeDoubleCheckSingleton singleton;public SafeDoubleCheckSingleton() {}// 双重锁设计public static SafeDoubleCheckSingleton getInstance() {if (singleton == null) {synchronized (SafeDoubleCheckSingleton.class) {if (singleton == null) {// 利用volatile,禁止“初始化对象(2)”和“设置singleton指向内存空间(3)"的重排序singleton = new SafeDoubleCheckSingleton();}}}return singleton;}
    }
    

    如果没有volatile在多线程的环境下该单列模式可能会产生线程安全问题。

使用限制

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁来保证原子性

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  • 变量不需要与其他的状态变量共同参与不变约束。

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

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

相关文章

PLC远程控制模块的通讯方式有哪些?工业网关ZP4000的功能与特点

在工业场景中&#xff0c;我们PLC通常采用有线的方式进行数据通讯&#xff0c;这种通讯方式距离受到局限&#xff0c;随着科技进步发展&#xff0c;人们更依赖于远程控制&#xff0c;以无线通讯的方式能够以更低成本的方式实现PLC远程控制管理。 在不同区域的PLC场景中&#x…

Django_POST请求的CSRF验证

目录 正常验证CSRF form表单 ajax的POST请求 关闭CSRF验证 源码等资料获取方法 django的POST接口发起请求默认清空下需要进行CSRF验证 正常验证CSRF form表单 如果form表单直接在标签之间添加{{ csrf_token }}就可以完成验证 ajax的POST请求 ajax的post需要在请求的he…

【监控系统】Prometheus监控组件Node-Exporter配置实战

这一节&#xff0c;我们来配置一下Node-Exporter&#xff0c;那么我们先来了解一下什么是Prometheus的Exporter&#xff1f; 任何向Prometheus提供监控样本数据的程序都可以被称为一个Exporter&#xff0c;它是一种用于将不同数据源的指标提供给Prometheus进行收集和监控的工具…

ITIL 4—监控和事态管理实践

1 关于本文 本文为监控和事态管理实践提供了实用指南。它分为五个主要部分&#xff0c;内容包括&#xff1a; 有关实践的一般信息监控和事态管理的流程和活动及其在服务价值链中的角色监控和事态管理中涉及的组织和人员支持监控和事态管理的信息和技术合作伙伴和供应商对监控…

Linux进度条小程序

文章目录 &#x1fa85;1. 回车换行♥1.1 回车♥1.2 换行 &#x1fa86;2. 缓冲区现象&#x1f9f8;3. 进度条实现♟3.1 逻辑♟3.2 进度条样式♟3.3 代码实现 &#x1f0cf;4. 场景使用 &#x1fa85;1. 回车换行 在学习C语言的时候&#xff0c;我们输出的时候&#xff0c;通常…

【Redis】高可用之二:哨兵(sentinel)

本文是Redis系列第5篇&#xff0c;前4篇欢迎移步 【Redis】不卡壳的 Redis 学习之路&#xff1a;从十大数据类型开始入手_AQin1012的博客-CSDN博客关于Redis的数据类型&#xff0c;各个文章总有些小不同&#xff0c;我们这里讨论的是Redis 7.0&#xff0c;为确保准确&#xff…

python OA流程图xml文件画图 graphviz的使用

下面的公文发文的流程图&#xff0c;虽然流程环节有坐标信息&#xff0c;但graphviz设置pos参数效果也不是太好 问题在于如何为流程环节设置绝对坐标 D:\Study\myproject\Python_auto_office\flow_report\utils\draw_image.py 通过xml流程文件绘制流程图 import graphviz …

使用OpenCV在图像上绘制质心

这段代码中已经实现了在图像上绘制质心的功能。质心,也称为重心,是物体质量分布的几何中心,可以通过物体质量和位置的加权平均来求得。 在这个程序中,图像的质心(重心)是通过计算像素强度(可以被看作是“质量”)的加权平均位置得到的。图像上每一个像素都有一个位置(…

谈谈VPN是什么、类型、使用场景、工作原理

作者&#xff1a;Insist-- 个人主页&#xff1a;insist--个人主页 作者会持续更新网络知识和python基础知识&#xff0c;期待你的关注 前言 本文将讲解VPN是什么、以及它的类型、使用场景、工作原理。 目录 一、VPN是什么&#xff1f; 二、VPN的类型 1、站点对站点VPN 2、…

怎么把word转换成只有一页的长页PDF?

来百度APP畅享高清图片 要将Word文档打印成一长页的PDF格式&#xff0c;我们得先知道word转PDF的工作原理。word转pdf其实就是将word打印出来&#xff0c;就是跟你用物理打印机打印的原理是差不多的&#xff0c;所不同的是&#xff0c;PDF虚拟打印的原理是利用虚拟打印机驱动程…

对Vue组件化开发思想的一些理解

目录 组件的分类 为什么需要组件化开发 如何设计组件 组件间通信 组件系统是 Vue的一个重要概念&#xff0c;让我们可以用独立可复用的小组件来构建大型应用。几乎任意类型的应用的界面都可以抽象为一个组件树&#xff1a; 写一个 Vue 项目&#xff0c;其实就是在写一个个的…

达梦sql执行计划、HINT、索引简单应用

目录 收集统计信息. 3 1. 通过DBMS_STATS包中的方法. 3 2、删除指定表的统计信息. 3 执行计划. 3 常用执行计划操作符. 4 统计指定sql执行号的所有操作符的执行时间. 5 HINT 5 并行操作&#xff1a;. 6 查询计划重用、结果集重用. 7 示例. 8 1、收集统计信息&#x…