volatile关键字修饰的共享变量主要有两个特点:1.保证了不同线程访问的内存可见性    2.禁止重排序

在说内存可见性和有序性之前,我们有必要看一下Java的内存模型(注意和JVM内存模型的区分)

为什么要有java内存模型?

首先我们知道内存访问和CPU指令在执行速度上相差非常大,完全不是一个数量级,为了使得java在各个平台上运行的差距减少,哪些搞处理器的大佬就在CPU上加了各种高速缓存,来减少内存操作和CPU指令的执行速度差距。而Java在java层面又进行了一波抽象,java内存模型将内存分为工作内存和主存,每个线程从主存load数据到工作内存,将load的数据赋值给工作内存上的变量,然后该工作内存对应的线程进行处理,处理结果在赋值给其工作内存,然后再将数据赋值给主存中的变量(这时候需要有一张图)。

使用工作内存和主存虽然加快了处理速度,但是也带来了一些问题,比如下面这个例子

         int i = 1;
i = i+1;

当在单线程情况下,i最后的值一定是2;但是在两个线程情况下一定是3吗?那就未必了。当线程A读取i的值为1,load到其工作内存,这时CPU切换至线程B,线程B读取i的值也是1,然后对加1然后save到主存,这时线程A也对i进行加1,也save回主存,但最终i的值为2。如果写操作比较慢,你读到的值还有可能是1,这就是缓存不一致的问题。JMM就是围绕着原子性,内存可见性,有序性这三个特征建立的。通过解决这个三个特征来解决缓存不一致的问题。而volatile主要针对于内存可见性和有序性。

原子性

原子性是指一个操作要么成功,那么失败,没有中间状态,比如i=1,直接读取i的值,这肯定是原子操作;但是i++,看似好像是,其实需要先读取i的值,然后+1,最后在赋值给i,需要三个步骤,这就不是原子性操作。在JDK1.5引入了boolean、long、int对应的原子性类AtomicBoolean、AtomicLong、AtomicInteger,他们可以提供原子性操作。

内存可见性

具有内存可见性的变量在被线程修改以后,会立刻刷新到主存并使其他线程的缓存行上的数据失效

volatile修饰的变量具有内存可见性,主要表现为:当写一个volatile变量时,JMM会将该线程对应的工作内存中的共享变量立即刷新到主存;当读一个volatile变量时,JMM会把该线程对应的工作内存中的值置为无效,然后从主存中进行读取,但是如果没有线程对该共享变量进行修改,则不会触发该操作。

有序性

JMM是允许处理器和编译器对指令进行重排序的,但规定了as-if-serial,即无论怎么重排序,最终结果都是一样的。比如下面这段代码:

         int weight = 10;                           //A
int high = 5; //B
int area = high * weight * high; //C

这段代码中可以按照A-->B-->C执行,也可以按照B-->A-->C执行,因为A和B是相互独立的,而C依赖于A、B,所以C不能排到A或B的前面。JMM保证了单线程的重排序,但是在多线程中就容易出现问题。比如下面这种情况

 boolean flag = false;
int a = 0; public void write(){
int a = 2; //
flag = true; //
}
public void multiply(){
if(flag){ //
int ret = a * a ; //
}
}

如果有两个线程执行上面的代码,线程1先执行write方法,随后线程2执行multiply方法。最后结果一定是4吗,不一定。

如图,JMM对1和2进行了重排序,先将flag设置为true,这是线程2执行,由于a还没有赋值,所以最后ret的值为0;

如果使用volatile关键字修饰flag,禁止重排序,可以保证程序的有序性,也可以使用synchronized或者lock这种重量级锁来保证有序性,但性能会下降。

另外,JMM具备一些先天的有序性,即不需要通过任何手段就可以保证的有序性,通常称为happens-before原则。<<JSR-133:Java Memory Model and Thread Specification>>定义了如下happens-before规则:

  1. 程序顺序规则: 一个线程中的每个操作,happens-before于该线程中的任意后续操作

  2. 监视器锁规则:对一个线程的解锁,happens-before于随后对这个线程的加锁

  3. volatile变量规则: 对一个volatile域的写,happens-before于后续对这个volatile域的读

  4. 传递性:如果A happens-before B ,且 B happens-before C, 那么 A happens-before C

  5. start()规则: 如果线程A执行操作ThreadB_start()(启动线程B) , 那么A线程的ThreadB_start()happens-before 于B中的任意操作

  6. join()原则: 如果A执行ThreadB.join()并且成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

  7. interrupt()原则: 对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生

  8. finalize()原则:一个对象的初始化完成先行发生于它的finalize()方法的开始

第1条规则程序顺序规则是说在一个线程里,所有的操作都是按顺序的,但是在JMM里其实只要执行结果一样,是允许重排序的,这边的happens-before强调的重点也是单线程执行结果的正确性,但是无法保证多线程也是如此。

第2条规则监视器规则其实也好理解,就是在加锁之前,确定这个锁之前已经被释放了,才能继续加锁。

第3条规则,就适用到所讨论的volatile,如果一个线程先去写一个变量,另外一个线程再去读,那么写入操作一定在读操作之前。

第4条规则,就是happens-before的传递性。

需要注意的是,被volatile修饰的共享变量只满足内存可见性和禁止重排序,并不能保证原子性。比如volatile i++。

 public class Test {
public volatile int inc = 0; public void increase() {
inc++;
} public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
} while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}

按道理来说结果是10000,但是运行下很可能是个小于10000的值。有人可能会说volatile不是保证了可见性啊,一个线程对inc的修改,另外一个线程应该立刻看到啊!可是这里的操作inc++是个复合操作啊,包括读取inc的值,对其自增,然后再写回主存。

假设线程A,读取了inc的值为10,这时候被阻塞了,因为没有对变量进行修改,触发不了volatile规则。

线程B此时也读读inc的值,主存里inc的值依旧为10,做自增,然后立刻就被写回主存了,为11。

此时又轮到线程A执行,由于工作内存里保存的是10,所以继续做自增,再写回主存,11又被写了一遍。所以虽然两个线程执行了两次increase(),结果却只加了一次。

有人说,volatile不是会使缓存行无效的吗?但是这里线程A读取到线程B也进行操作之前,并没有修改inc值,所以线程B读取的时候,还是读的10。

又有人说,线程B将11写回主存,不会把线程A的缓存行设为无效吗?但是线程A的读取操作已经做过了啊,只有在做读取操作时,发现自己缓存行无效,才会去读主存的值,所以这里线程A只能继续做自增了。

综上所述,在这种复合操作的情景下,原子性的功能是维持不了了。但是volatile在上面那种设置flag值的例子里,由于对flag的读/写操作都是单步的,所以还是能保证原子性的。

要想保证原子性,只能借助于synchronized,Lock以及并发包下的atomic的原子操作类了,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。

volatile底层原理

如果将使用volatile修饰的代码和未使用volatile修饰的代码都编译成汇编语言,会发现,使用volatile修饰的代码会多出一个lock前缀指令。

lock前缀指令相当于一个内存屏障,内存屏障的作用有以下三点:

①重排序时,不能把内存屏障后面的指令排序到内存屏障前

②使得本CPU的cache写入内存

③写入动作会引起其他CPU缓存或内核的数据无效,相当于修改对其他线程可见。

volatile的应用场景

因为volatile对复合操作无效,所以volatile修饰像上面例子中的flag这样的只会发生读/写的标记型字段。

在单利模式中,volatile还可以修饰成员变量,防止初始化时的指令重排序。

 class Singleton{
private volatile static Singleton instance= null; private Singleton(){ } public static Singleton getInstance(){
if(instance==null){
synchronized(Singleton.class){
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}

Volatile的详解的更多相关文章

  1. Java volatile关键字详解

    Java volatile关键字详解 volatile是java中的一个关键字,用于修饰变量.被此关键修饰的变量可以禁止对此变量操作的指令进行重排,还有保持内存的可见性. 简言之它的作用就是: 禁止指 ...

  2. Java之先行发生原则与volatile关键字详解

    volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制,但是它并不容易完全被正确.完整地理解,以至于许多程序员都习惯不去使用它,遇到需要处理多线程数据竞争问题的时候一律使用synchro ...

  3. volatile关键字详解

    本文系转载,原文链接:http://www.cnblogs.com/Chase/archive/2010/07/05/1771700.html,如有侵权,请联系我:534624117@qq.com 引 ...

  4. volatile使用详解

    Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized”:与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少, ...

  5. Java中Volatile关键字详解

    一.基本概念 先补充一下概念:Java并发中的可见性与原子性 可见性: 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值, ...

  6. C/C++中volatile关键字详解 (转)

    1. 为什么用volatile? C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,通常用于建立语言级别的 memory barrier.这是 BS 在 "The ...

  7. C/C++中volatile关键字详解

    1. 为什么用volatile? C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,通常用于建立语言级别的 memory barrier.这是 BS 在 "The ...

  8. Java并发编程:JMM (Java内存模型) 以及与volatile关键字详解

    目录 计算机系统的一致性 Java内存模型 内存模型的3个重要特征 原子性 可见性 有序性 指令重排序 volatile关键字 保证可见性和防止指令重排 不能保证原子性 计算机系统的一致性 在现代计算 ...

  9. Java多线程-----volatile关键字详解

       volatile原理     Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程.当把变量声明为volatile类型后, 编译器与运行时都会注意 ...

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

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

随机推荐

  1. Android 自定义圆形图表

    <com...SignChartView android:id="@+id/signChart" android:layout_width="265dp" ...

  2. 域名和DNS服务器

    概念性的东西: 域名:     ①.百度:域名(Domain Name),是由一串用点分隔的名字组成的Internet上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的电子方位(有时也指地理 ...

  3. haproxy学习——安装(一)

    安装包:haproxy-1.5.4.tar.gz (挺小的,大约1.3M) ①.首先要sz到本地虚拟机上(centos-6.5),tar zxvf haproxy-1.5.4.tar.gz,完成解压. ...

  4. SpringBoot 启动参数设置环境变量、JVM参数、tomcat远程调试

    java命令的模版:java [-options] -jar jarfile [args...] 先贴一下我的简单的启动命令: java -Xms128m -Xmx256m -Xdebug -Xrun ...

  5. how reset smartphone data.

    question:how  reset  meizu smartphone solution one:hard step 1. power off your MEIZU smartphone. ste ...

  6. 服务器常用命令之 启用/禁用PING状态

    启用 echo 0 > /proc/sys/net/ipv4/icmp_echo_ignore_all  (PING的通) 禁用 echo 1 > /proc/sys/net/ipv4/i ...

  7. Asp.net网站优化【转】

    阅读目录 开始 配置OutputCache 启用内容过期 解决资源文件升级问题 启用压缩 删除无用的HttpModule 其它优化选项 本文将介绍一些方法用于优化ASP.NET网站性能,这些方法都是不 ...

  8. 20165322 预备作业3 Linux安装及学习

    Linux安装及学习 安装部分 由于是第一次接触虚拟机知识,之前也没什么了解,我选择完全按照老师教程里的安装vbox虚拟机. 虚拟机安装的过程很顺利,不做详细讲解. 出现的问题 在启动我新建的虚拟电脑 ...

  9. softmax实现cifar10分类

    将cifar10改成单一通道后,套用前面的softmax分类,分类率40%左右,想哭... .caret, .dropup > .btn > .caret { border-top-col ...

  10. 【[JLOI2014]松鼠的新家】

    //第一次A掉紫题就来写题解,我是不是疯了 //说实话这道题还是比较裸的树上差分 //对于树上的一条路径(s,t),我们只需要把ch[s]++,ch[t]++,ch[LCA(S,T)]--,再把lca ...