Java volatile关键字内存原语

一、简述

    volatile特性:实现最轻量级的同步。

    volatile关键字的内存原语主要包含2个:

1、保证volatile修饰的变量对所有线程的可见性。

2、禁止指令重排序优化。

 

二、案例代码

    先给一个经典的错误案例:

package com.hy.current;
public class VolatileTest {
     private static volatile int race = 0;
     public static void increase() {
           race++;
     }
     public static void main(String[] args) {
           Thread[] threads = new Thread[20];
           for (int i = 0; i < threads.length; i++) {
                threads[i] = new Thread(new Runnable() {
                     @Override
                     public void run() {
                           for (int j = 0; j < 10000; j++) {
                                increase();
                           }
                     }
                });
                threads[i].start();
           }
           while(Thread.activeCount() > 1) {
                Thread.yield();
           }
           System.out.println(race);
     }
 
}

正确的结果应该是200000,但程序运行的值均比200000小,为何?

javap -v com.hy.current.VolatileTest 得到字节码:  

public static void increase();
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #10                 // Field race:I
         3: iconst_1
         4: iadd
         5: putstatic     #10                 // Field race:I
         8: return
      LineNumberTable:
        line 8: 0
        line 9: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature

race++不是原子性操作,虽然volatile修改的变量能够保证线程拿到的值在当前时刻是最新的(getstatic),但其他线程可能执行到了iadd等操作,导致拿到的值已经过期,这样就会出现最终结果比预期的200000要小的原因。

这个是单独使用volatile同步失败的典型案例,此案例的正确做法是在increase()方法增加同步锁(synchronized或Lock都行)。

再来看一个正确的案例:

package com.hy.current;
public class VolatileTest2 {
     private static volatile boolean isShutdown = false;
     public void shutdown() {
           isShutdown = true;
     }
     public void execute() {
           while(!isShutdown) {
                // TODO
                // 执行业务代码
           }
     }
}

字节码如下:  

public void shutdown();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: iconst_1
         1: putstatic     #10                 // Field isShutdown:Z
         4: return
      LineNumberTable:
        line 8: 0
        line 9: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       5     0  this   Lcom/hy/current/VolatileTest2;

针对volatile变量只有一个赋值操作,并且是原子的,这样就能达到预期的同步结果。

我们简单总结一下用volatile做同步的场景:

1、对volatile变量的操作,必须是原子性的。或者可以保证volatile变量只能由一个线程来修改,其他线程只是使用此volatile变量。 

2、单独由这一个volatile变量控制同步,不能与其他变量一起参与。

三、Java内存模型

    要理解volatile关键字的作用,就先要了解Java内存模型,Java内存模型(Java Memory Model, JMM)指的是由Java虚拟机规范定义的,减少不同的操作系统平台内存访问的差异,实现跨平台一致性的内存访问效果,主要的目标就是定义程序中各个变量的访问规则,这里的变量指的是实例字段,静态字段和构成数组对象的元素,但不包括局部变量与方法参数,后者是线程私有的。

    JMM的内存有主内存和工作内存之分,可以狭义地认为,主内存主要对应Java堆中的对象实例,工作内存则对应虚拟机栈中的部分区域。主内存存储所有的变量,工作内存保存该线程使用到的变量 的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必须在工作内存中进行,而不能直接使用主内存的变量,不同的线程之间也无法直接访问对方工作内存中的变量,线程、工作内存、主内存三者关系图如下所示:

四、内存间交互操作

    此节主要描述主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存的实现细节,是本篇的重点。

    共有8种操作,每一种担任 都是原子的,如下图所示:

1、Lock:只在主内存中操作,将一个变量标识为被一条线程独占的状态。

2、unLock:与Lock对应,释放变量的锁定状态。

3、read:从主内存中读取一个变量传输到线程的工作内存中。

4、load:紧接着read操作之后,在工作内存开辟一个变量副本,装载read回来的变量。

5、use:把工作内存中的变量传递给执行引擎。

6、assign:与use对应,执行引擎向工作内存传输变量值(一般是变量的赋值操作)。

7、store:对应read操作,从工作内存读取一个变量传输到主内存。

8、write:对应load操作,紧接着store之后,在主内存中开辟一段空间,保存store回来的变量。

看手绘图可知,看似8个操作挺复杂,整理一下可以知道,是相对应的4对操作。

JMM规定这8个操作必须满足的条件:

1、read/load或是store/write这两组操作,必须成对出现,保证值一定可以正常读取、写入。

2、assign操作后面必须会有store/write这组操作,不允许出现工作内存有变量更新,不刷新回主内存的现象存在。

3、没有assign就不会平白无故地出现store/write这组操作。

4、一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化的变量,即有use/store操作出现,前面肯定会有assign/load操作。

5、一个变量在同一个时刻只能被一个线程Lock,Lock操作可以执行多次,想到解锁,就必须unLock相同的次数。

6、线程对一个变量进行Lock操作,将会清掉线程此变量在工作内存中的值,想用这个变量,就要重新load->assign操作。

7、没Lock的变量是不能执行unLock的。

8、unLock执行前,必须先执行store->write将此变量刷新到主内存中。

 

五、禁止指令重排序

    变量加了volatile修饰后,汇编代码里会多出一个"lock addl $0x0,(%esp)"操作,这种操作相当于一个内存屏障(Memory Barrier或Memory Fence,指重排序时不能把后面的指令重排序到内存屏障之前的位置),这样就可以保证volatile修饰的变量,是先赋值,再使用。

    如果没有volatile修饰,指令重排序操作(属于硬件架构CPU优化的范畴,简单来说就是CPU在保证正确处理指令依赖、执行结果正确的前提下,会对一些指令进行顺序的优化,这样指令和实际的代码顺序就会有一些差别)会影响线程之间共享变量的使用。

举个例子:

package com.hy.current;
import java.io.IOException;
import java.util.Properties; 
/**
 * @author Administrator
 *
 */
public class VolatileTest3 { 
     private static boolean isInitial = false;
     /**
      * 加载配置信息,加载完成后isInitial设置为true
      * 由线程A调用
      */
     public void init() throws IOException {
           Properties prop = new Properties();
          prop.load(VolatileTest3.class.getClassLoader().getResourceAsStream("test.properties"));
           isInitial = true;
     }
 
     /**
      * 如果配置信息加载完成后,执行相关业务代码
      * 由线程B调用
      */
 
     public void execute() {
           if (isInitial) {
                // TODO 执行业务代码
           }
     }
}

乍一看,好像没问题,运行起来也能得到预期结果,但是请思考一下这行代码的位置:

isInitial = true;

一定能保证配置信息加载完后才执行吗?

我们看看这个方法,isInitial在方法里跟其他变量没有任何的依赖,CPU完全有优化的可能将isInitial的赋值操作放在最前面,此致"isInitial = true;"被提前执行,这样就会导致配置信息没加载完,线程B就已经开始干活了。

    isInitial变量增加volatile修饰后,就可以避免这种排序情况发生,从而保证了线程B的起始条件的正确的。

 

六、volatile变量定义在Java内存模型中的特殊规则

    前面我们讲到JMM中内存的交互操作,如果变量用volatile修饰,JMM中有三条特殊的规则:

    1、普通变量只要求load、read动作相关联,volatile变量要求use、load、read三个动作相关联,意思就是想要用的变量,每次都是从主内存取的。

    2、普通变量只要求store、write动作相关联,volatile变量要求assign、store、write三个动作相关联,意思就是有变化的变量,立即刷新到主内存中。

    3、use/assign、load/store、read/ write这三组我们先认为是三段操作,假定一个线程T、两个变量V、W,如果线程T对变量V的use/assign先于对变量W的use/assign操作,那么线程T对变量V的load/store也先于对变量W的load/store操作,意思就是线程对volatile变量的读操作(use、load、read)或写操作(assign、store、write)都会整套执行完,再去执行另一个volatile的整套读写操作,这条规则体现出来的特性就是禁止指令重新排序优化,保证代码的执行顺序与程序的顺序相同。

七、总结

    1、volatile是最轻量的同步方案最佳实践,但要注意使用的场景。

    2、JMM内存交互操作,以及volatile后的特殊规则是如何达到变量对所有线程可见性的实现原理。

来源《深入理解Java虚拟机》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值