更多内容,前往IT-BLOG

volatile 表示 “不稳定” 的意思。用于修饰共享可变变量,即没有使用 final(不可变变量) 关键字修饰的实例变量或静态变量,相应的变量就被称为 volatile 变量,如下:

private volatile String name;

volatile 关键字修饰的变量容易变化(多线程下会被其它线程修改),因而不稳定。volatile 变量的不稳定性意味着对这种变量的读和写操作都必须从高速缓存或者主内存(也是通过高速缓存读取),以读取变量的最新值。因此,volatile 变量不会被编译器分配到寄存器进行存储(相当于备份),对 volatile 变量的读写操作都是内存访问(访问高速缓存相当于主内存)操作。

Volatile 被称为轻量级锁,其作用与锁的作用有相同的地方:保证可见性和有序性。所不同的是,在原子性方面它仅能保障写 volatile 变量操作的原子性,但没有锁的排他性;volatile 关键字的使用不会引起上下文切换(这是 volatile 被冠以 “轻量级” 的原因)。

一、volatile 的作用


volatile 关键字的作用包括:保障可见性、有序性和 long/double 型变量读写操作的原子性。一般而言,对 volatile 变量的赋值操作,其右边表达式中只要涉及共享变量(包括被赋值的 volatile 变量本身),那么这个赋值操作就不是原子操作。要保障这样操作的原子我们仍需要借助锁。

volatile Map map = new HashMap(); 可以分解为如下伪代码所示的几个子操作:
【1】objRef = allocate(HashMap.class);  //子操作①:分配对象所需的存储空间。
【2】invokeConstructor(objRef); //子操作②:初始化 objRef 引用的对象。
【3】map = objRef; //子操作③:将对象引用写入变量 map。
虽然 volatile 关键字仅保障其中的子操作③是一个原子操作,但是由于子操作①和子操作②仅涉及局部变量而未涉及共享变量,因此对变量 map 的赋值操作仍然是一个原子操作。

写线程对 volatile 变量的写操作会产生类似于释放锁的效果。读线程对 volatile 变量的读操作会产生类型于获得锁的效果。因此,volatile 具有保障有序性和可见性的作用。

对于 volatile 变量的写操作,Java 虚拟机会在该操作之前插入一个释放屏障[禁止 volatile 写操作与该操作之前的任何读、写操作进行重排序,从而保证了 volatile 写操作之前的任何读、写操作会先于 volatile 写操作被提交],并在该操作之后插入一个存储屏障[具有冲刷处理器缓存的作用,因此在 volatile 变量写操作之后插入的一个存储屏障就使得该存储屏障前所有操作的结果(包括 volatile 变量写操作及该操作之前的任何操作)对其他处理器来说是可同步的],如下图:

对于 volatile 变量读操作,Java 虚拟机会在该操作之前插入一个加载屏障(Load Barrier),并在该操作之后插入一个获取屏障(Acquire Barrier),如下图:

加载屏障通过冲刷处理器缓存,使其执行线程(读线程)所在的处理器将其他处理器对共享变量(可能是多个变量)所做的更新同步到该处理器的高速缓存中。读线程执行的加载屏障和写线程执行的存储屏障配合在一起使得写线程对 volatile 变量的写操作以及在此之前所执行的其他内存操作的结果对读线程可见,即保障了可见性。因此 volatile 不仅仅保障了 volatile 变量本身的可见性,还保障了写线程在更新 volatile 变量之前执行的所有操作的结果对读线程可见。这种可见性保障类似于锁对可见性的保障,与锁不同的是 volatile 不具备排他性,因而它不能保障读线程读取到的这些共享变量的值是最新的,即读线程读取到这些共享变量的那一刻可能已经有其他写线程更新了这些共享变量的值。另外,获取屏障禁止了 volatile 读操作之后的任何操作读,写操作与volatile 读操作进行重排序。因此它保障了 volatile 读操作之后的任何操作开始执行之前,写操作对相关共享变量(包括 volatile 变量和普通变量)的更新已经对当前线程可见。
【volatile 在有序性保障方面也可以从禁止重排序的角度理解,即 volatile 禁止了如下重排序】:
【1】写 volatile 变量操作与该操作之前的任何读、写操作不会被重排序;
【2】读 volatile 变量操作与该操作之后的任何读、写操作不会被重排序;

二、volatile 变量的开销


volatile 变量的开销包括读变量和写变量两个方面。volatile 变量的读、写操作都不会导致上下文切换,因此 volatile 的开销比锁要小。写一个 volatile 变量会使该操作以及该操作之前的任何写操作的结果对其他处理器是可同步的,因此 volatile 变量写操作的成本介于普通变量的写操作和在临界区进行的写操作之间。读取 volatile 变量的成本也比在临界区中读取变量要低(没有锁的申请与释放以及上下文切换的开销),但是其成本可能比读取普通变量要高一些。这是因为 volatile 变量的值每次都需要从高速缓存或者主内存中读取,而无法被暂存在寄存器中,从而无法发挥访问的高效性。

三、volatile 的典型应用场景


场景一:使用 volatile 变量作为状态标志。在该场景中,应用程序的某个状态由一个线程设置,其他线程会读取该状态并以该状态作为其计算的依据(或者仅仅读取并输出这个状态值)。此时使用 volatile 变量作为同步机制的好处是一个线程能够“通知”另外一个线程某种事件(例如,网络连接断连之后重新连上)的发生,而这些线程又无须使用锁,从而避免了锁的开销以及相关问题。
场景二:使用 volatile 保障可见性。在该场景中,多个线程共享一个可变状态变量,其中一个线程更新了该变量之后,其他线程在无须加锁的情况下也能够看到该更新。
场景三:使用 volatile 变量代替锁。volatile 关键字并非锁的代替品,但是在一定的条件下它比锁更合适(性能开销小,代码简单)。多个线程共享一组可变状态变量的时候,通常我们需要使用锁来保障对这些变量的更新操作的原子性,以避免产生不一致问题。利用 volatile 变量写操作具有的原子性,我们可以把这一组可变状态变量封装成一个对象,那么对这些状态变量的更新操作就可以通过创建一个新的对象并将该对象引用赋值给相应的引用型变量来实现。在这个过程中,volatile 保障了原子性和可见性,从而避免了锁的使用。

volatile 关键字并非锁的替代品,volatile 关键字和锁各有其使用条件。前者更适合与多个线程共享一个状态变量(对象),而后者更适合于多个线程共享一组状态变量。某些情形下,我们可以将多个线程共享的一组状态变量合并成一个对象,用一个 volatile 变量来引用该对象,从而使我们不必要使用锁。

场景四:使用 volatile 实现简易版读写锁。在该场景中,读写锁是通过混合使用锁和 volatile 变量而实现的,其中锁用于保障共享变量写操作的原子性,volatile 变量用于保障共享变量的可见性。因此,与 ReentrantReadWriteLock 所实现的读写锁不同的是,这种简易版读写锁仅涉及一个共享变量并且允许一个线程读取这个共享变量时其他线程可以更新该变量(这是因为读线程并没有加锁)。因此,这种读写锁允许读线程可以读取到共享变量的非最新值。该场景的一个典型例子是实现一个计数器:

 1 public class Counter {
2 private volatile long count;
3 public long value(){
4 return count;
5 }
6
7 public void increment() {
8 synchronize(this){
9 count++;
10 }
11 }
12 }

四、volatile 实现可见性的原理


在 Java 并发编程中,一定绕不开 volatilesynchronizelock 几个关键字,其中 volatile 关键字是用来解决共享变量(类成员变量、类的静态成员变量等)的可见性问题,非共享成员变量(局部变量)是分配在 JVM 虚拟机的栈中,是线程私有的,不涉及可见性问题。
可见性在 JAVA规范中是这样定义的:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。通俗的讲就是如果有一个共享变量N,当有两个线程T1、T2同时获取了N的值,T1修改N的值,而T2读取N的值。那么可见性规范要求T2读取到的必须是T1修改后的值,而不能在T2读取旧值后T1修改为新值。volatile关键字修饰的共享变量可以提供这种可见性规范,也叫做读写可见。那么底层实现是通过机制保证 volatile变量读写可见的?Volatile的实现机制:在说这个问题之前,我们先看看CPU是如何执行 Java 代码的。

首先 Java代码会被编译成字节码 .class文件,在运行时会被加载到 JVM中,JVM会将 .class转换为具体的 CPU执行指令,CPU加载这些指令逐条执行。

以多核CPU为例(两核),我们知道 CPU的速度比内存要快得多,为了弥补这个性能差异,CPU 内核都会有自己的高速缓存区,当内核运行的线程执行一段代码时,首先将这段代码的指令集进行缓存行填充到高速缓存,如果非 volatile 变量当 CPU执行修改了此变量之后,会将修改后的值回写到高速缓存,然后再刷新到内存中。如果在刷新回内存之前,由于是共享变量,那么CORE2 中的线程执行的代码也用到了这个变量,这是变量的值依然是旧的。volatile关键字就会解决这个问题的,如何解决呢,首先被 volatile关键字修饰的共享变量在转换成汇编语言时,会加上一个以 lock为前缀的指令,当CPU发现这个指令时,立即做两件事:
【1】将当前内核高速缓存行的数据立刻回写到内存;
【2】使在其他内核里缓存了该内存地址的数据无效;
第一步很好理解,第二步如何做到呢?
【MESI协议】:在早期的 CPU中,是通过在总线加 LOCK#锁的方式实现的,但这种方式开销太大,所以 Intel开发了缓存一致性协议,也就是 MESI协议[伊利诺斯],该解决缓存一致性的思路是:当 CPU写数据时,如果发现操作的变量是共享变量,即在其它 CPU中也存在该变量的副本,那么它会发出信号通知其他 CPU将该变量的缓存行设置为无效状态。当其它 CPU使用这个变量时,首先会去嗅探是否有对该变量更改的信号,当发现这个变量的缓存行已经无效时,会从新从内存中读取这个变量。以上这些就是 volatile关键字的内部实现机制。

volatile 关键字(轻量级同步机制)的更多相关文章

  1. Java的synchronized关键字:同步机制总结

    JAVA中synchronized关键字能够作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块.搞清楚synchronized锁定的是哪个对象,就能帮助我们设计更安全的多线程程 ...

  2. Java 中 volatile 关键字及其作用

    引言 作为 Java 初学者,几乎从未使用过 volatile 关键字.但是,在面试过程中,volatile 关键字以及其作用还是经常被面试官问及.这里给各位童靴讲解一下 volatile 关键字的作 ...

  3. Java并发编程:Java中的锁和线程同步机制

    锁的基础知识 锁的类型 锁从宏观上分类,只分为两种:悲观锁与乐观锁. 乐观锁 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新 ...

  4. JUC(一):volatile关键字

    volatile是什么 ​ volatile是java虚拟机提供的轻量级同步机制,它包含三种特性: 保证可见性:只要主内存中变量做出修改,其余线程马上会感知到变量的修改. package com.ch ...

  5. Java多线程 | 02 | 线程同步机制

    同步机制简介 ​ 线程同步机制是一套用于协调线程之间的数据访问的机制.该机制可以保障线程安全.Java平台提供的线程同步机制包括: 锁,volatile关键字,final关键字,static关键字,以 ...

  6. Java并发之volatile关键字

    引言 说到多线程,我觉得我们最重要的是要理解一个临界区概念. 举个例子,一个班上1个女孩子(临界区),49个男孩子(线程),男孩子的目标就是这一个女孩子,就是会有竞争关系(线程安全问题).推广到实际场 ...

  7. 轻量级的同步机制——volatile语义详解(可见性保证+禁止指令重排)

    目录 1.关于volatile 2.语义一:内存可见性 2.1 一个例子 2.2 java的内存模型(JMM) 2.3 happens-before规则 2.4 volatile解决内存可见性问题的原 ...

  8. java并发:线程同步机制之Volatile关键字&原子操作Atomic

    volatile关键字 volatile是一个特殊的修饰符,只有成员变量才能使用它,与Synchronized及ReentrantLock等提供的互斥相比,Synchronized保证了Synchro ...

  9. java 轻量级同步volatile关键字简介与可见性有序性与synchronized区别 多线程中篇(十二)

    概念 JMM规范解决了线程安全的问题,主要三个方面:原子性.可见性.有序性,借助于synchronized关键字体现,可以有效地保障线程安全(前提是你正确运用) 之前说过,这三个特性并不一定需要全部同 ...

  10. jvm运行机制和volatile关键字详解

    参考https://www.cnblogs.com/dolphin0520/p/3920373.html JVM启动流程 1.java虚拟机启动的命令是通过java +xxx(类名,这个类中要有mai ...

随机推荐

  1. MVC+EF API 跨域

    MVC+EF API --2 一. MVC+EF 不管是MvcHAIS Ef 都有文件夹Controller 二.Link查询 多表联查 匿名类型 三.Postman使用 四.mvc访问使用API 跨 ...

  2. 22 BootStrapModelForm

    方便之处在于,我们不会再一遍一遍的写form的样式了. from django import forms class BootStrapModelForm(forms.ModelForm): def ...

  3. 【Linux】Ubuntu随笔

    Ubuntu声明环境变量时使用 export JAVA_HOME=/xx/xx/xx,当需要引用时要写成 $JAVA_HOME 所以配置环境变量并声明方法如下: vim ~/.bashrc expor ...

  4. UE C++教程之接口 UINTERFACE

    我是谁不重要,重要的是,我能做什么. 近期笔者在进行UE的开发时,实现多武器的换弹与开火需要用到接口.而笔者以前是做Unity开发的,遂没有使用过UE C++的UINTERFACE,而这个接口在使用过 ...

  5. C#基于数据库链接增删改查

    一.创建一个winfrom窗体 1.创建项目 2.创建一个链接数据的类 3.封装数据库的实体类(查询和增加) 在对数据操作时必须引用连个数据库using using System.Data; usin ...

  6. hdu: 改革春风吹满地(叉乘求面积)

    Problem Description" 改革春风吹满地,不会AC没关系;实在不行回老家,还有一亩三分地.谢谢!(乐队奏乐)" 话说部分学生心态极好,每天就知道游戏,这次考试如此简 ...

  7. webpack配置跨域proxy

    首先新建一个项目: 安装vue-cli: npm i -g @vue/cli npm i -g @vue/cli-init 安装webpack: npm install webpack -g vue新 ...

  8. 设计模式 - 单例模式 Singleton Pattern - C#

    单例模式 Singleton Pattern 1.单例模式设计模式属于创建型模式 2.是单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建.这个类提供了一种访问其唯一的对象的方式,可以直接访 ...

  9. 转 oracle 无法使用sys用户登录 connection as SYS should be as SYSDBA OR SYSOPER

    转自:  https://blog.csdn.net/u012004128/article/details/80781979 安装Oracle11g后,为了测试安装是否成功,通过cmd命令打开了sql ...

  10. Windows查看CUDA版本

    桌面右击,查看是否有NVIDIA控制面板 打开控制面板->帮助->系统信息->组件,可以看到CUDA版本