当前位置: 首页 > news >正文

volatile 底层原理以及特性详解

如果大家对java架构相关感兴趣,可以关注下面公众号,会持续更新java基础面试题, netty, spring boot,spring cloud等系列文章,一系列干货随时送达, 超神之路从此展开, BTAJ不再是梦想!

架构殿堂

概念

1 volatile变量,用来确保将变量的更新操作通知到其他线程。

2 当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。

3 volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

特性

假如一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,具备以下特性:

1、保证多线程下的可见性

2、对于单个的共享变量的读/写具有原子性,无法保证类似num++的原子性。

3、禁止进行指令重排序(即保证有序性)。即volatile前面的代码先于后面的代码先执行

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。在Java里面,可以通过volatile关键字来保证一定的“有序性”。

Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,也就是happens-before 原则。

happens-before 原则

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

Java内存模型 (Java Memory Model,JMM)

Java内存模型由Java虚拟机规范定义,用来屏蔽各个平台的硬件差异。简单来说:

1 所有变量储存在主内存。
2 每条线程拥有自己的工作内存,其中保存了主内存中线程使用到的变量的副本。
3 线程不能直接读写主内存中的变量,所有操作均在工作内存中完成。

线程,主内存,工作内存的交互关系如图。
在这里插入图片描述

和volatile有关的操作为:

read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

为什么要使用Volatile

Volatile变量修饰符如果使用恰当的话,它比synchronized的使用和执行成本会更低,因为它不会引起线程上下文的切换和调度。

volatile的原子性问题

volatile仅仅保障对其修饰的变量的写操作( 以及读操作 )本身的原子性 ,而这并不表示对 volatile 变量的赋值操作一定具有原子性。例如,如下对volatile 变量 count1的赋值操作并不是原子操作:

count1 = count2 + 1;

如果变量count2也是一个共享变量,那么该赋值操作实际上是一个read-modify-write 操作。其执行过程中其他线程可能已经更新了 count2 的值,因此该操作不具备不可分割性,也就不是原子操作。如果变量count2 是一个局部变量,那么该赋值操作就是一个原子操作。

对volatile变量的赋值操作,其右边表达式中只要涉及共享变量 ( 包括被赋值的 volatile 变量本身 ),那么这个赋值操作就不是原子操作。要保障这样操作的原子性, 仍然需要借助锁。

解决num++操作的原子性问题

针对num++这类复合类的操作,可以使用java并发包中的原子操作类原子操作类是通过循环CAS的方式来保证其原子性的。

public class Counter {  //使用原子操作类
public static AtomicInteger num = new AtomicInteger(0);
//使用CountDownLatch来等待计算线程执行完
static CountDownLatch countDownLatch = new CountDownLatch(30);
public static void main(String []args) throws InterruptedException {
    //开启30个线程进行累加操作
    for(int i=0;i<30;i++){
        new Thread(){
            public void run(){
                for(int j=0;j<10000;j++){
                    num.incrementAndGet();//原子性的num++,通过循环CAS方式
                }
                countDownLatch.countDown();
            }
        }.start();
    }
    //等待计算线程执行完
    countDownLatch.await();
    System.out.println(num);
}
}

实现原理

可见性实现原理

将一个共享变量声明为volatile后,会有以下效应

1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;

2.这个写会操作会导致其他线程中的缓存无效。

volatile能够保证可见性,那么它是如何实现可见性的呢?以X86处理器为例,在对volatile修饰的变量进行写操作时,通过编译器生成反汇编指令后,会发现会多一条Lock前缀,就是由于这条Lock前缀所实现的可见性。Lock前缀在多核处理器中会引发下面这两件事情:

​ 1Lock指令会将当前处理器缓存行的数据写回到主内存。(ps:每个处理器都有自己的cache缓存,每次缓存中操作的变量都是主内存中变量的拷贝)
​ 2 一个处理器写回主内存的操作会造成其他处理的缓存无效。

禁止指令重排原理

​ 通过内存屏障来实现禁止指令重排。

如图
在这里插入图片描述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FexTyina-1591706164985)(E:\技术帖子\笔记\基础\图\volatile\内存屏障.png)]

volatile的使用优化

在JDK7的并发包里新增了一个队列集合类LinkedTransferQueue,它在使用volatile变量的时候,会采用一种将字节追加到64字节的方法来提高性能。

追加到64字节能够优化性能原因

在很多处理器中它们的L1、L2、L3缓存的高速缓存行都是64字节宽,不支持填充缓存行,例如,现在有两个不足64字节的变量AB,那么在AB变量写入缓存行时会将AB变量的部分数据一起写入一个缓存行中,那么在CPU1和CPU2想同时访问AB变量时是无法实现的,也就是想同时访问一个缓存行的时候会引起冲突,如果可以填充到64字节,AB两个变量会分别写入到两个缓存行中,这样就可以并发,同时进行变量访问,从而提高效率。

总结

volatile是一种轻量级的同步机制,它主要有三个特性:

一是保证共享变量对所有线程的可见性

二是禁止指令重排序优化

三是volatile对于单个的共享变量的读/写具有原子性,无法保证类似num++的原子性,需要通过循环CAS的方式来保证num++操作的原子性。

如果大家对java架构相关感兴趣,可以关注下面公众号,会持续更新java基础面试题, netty, spring boot,spring cloud等系列文章,一系列干货随时送达, 超神之路从此展开, BTAJ不再是梦想!

架构殿堂

相关文章:

  • js---05 自定义属性
  • 深入解析JMM原理
  • 为Redmine的项目加上起止时间
  • 详尽Netty(一):初探netty
  • Php基础知识测试题
  • 设计模式(四):建造者模式的详细解析
  • [swust1745] 餐巾计划问题(费用流)
  • 详尽Netty(二):源码环境搭建
  • AssetBundle管理机制(上)
  • 详尽Netty(三):Channel
  • 循序渐进之Spring AOP(5) - 创建切面
  • 牛逼!阿里推出国产开源的jdk! 快来试试吧!
  • ES6 中的let 声明变量
  • 原来Java类的加载过程是这样的?
  • 淘宝数据库OceanBase SQL编译器部分 源代码阅读--生成物理查询计划
  • [nginx文档翻译系列] 控制nginx
  • 【node学习】协程
  • 2017前端实习生面试总结
  • 30秒的PHP代码片段(1)数组 - Array
  • FastReport在线报表设计器工作原理
  • gcc介绍及安装
  • HTTP那些事
  • mysql中InnoDB引擎中页的概念
  • PHP变量
  • Python利用正则抓取网页内容保存到本地
  • Redux 中间件分析
  • spring boot 整合mybatis 无法输出sql的问题
  • STAR法则
  • 对超线程几个不同角度的解释
  • 给新手的新浪微博 SDK 集成教程【一】
  • 互联网大裁员:Java程序员失工作,焉知不能进ali?
  • 利用jquery编写加法运算验证码
  • 跳前端坑前,先看看这个!!
  • 我的面试准备过程--容器(更新中)
  • 我有几个粽子,和一个故事
  • [Shell 脚本] 备份网站文件至OSS服务(纯shell脚本无sdk) ...
  • PostgreSQL之连接数修改
  • ​ ​Redis(五)主从复制:主从模式介绍、配置、拓扑(一主一从结构、一主多从结构、树形主从结构)、原理(复制过程、​​​​​​​数据同步psync)、总结
  • # MySQL server 层和存储引擎层是怎么交互数据的?
  • (11)工业界推荐系统-小红书推荐场景及内部实践【粗排三塔模型】
  • (2)(2.10) LTM telemetry
  • (20050108)又读《平凡的世界》
  • (42)STM32——LCD显示屏实验笔记
  • (附源码)计算机毕业设计SSM在线影视购票系统
  • (官网安装) 基于CentOS 7安装MangoDB和MangoDB Shell
  • (求助)用傲游上csdn博客时标签栏和网址栏一直显示袁萌 的头像
  • (收藏)Git和Repo扫盲——如何取得Android源代码
  • (原)Matlab的svmtrain和svmclassify
  • .Net MVC + EF搭建学生管理系统
  • .NET/C# 使用 #if 和 Conditional 特性来按条件编译代码的不同原理和适用场景
  • .NET/C# 中设置当发生某个特定异常时进入断点(不借助 Visual Studio 的纯代码实现)
  • .net中调用windows performance记录性能信息
  • .NET中两种OCR方式对比
  • /dev下添加设备节点的方法步骤(通过device_create)
  • @autowired注解作用_Spring Boot进阶教程——注解大全(建议收藏!)