400-616-5551

您所在位置: 首页> 学习课程> java培训 | Java volatile关键字内存原语

java培训 | Java volatile关键字内存原语

发布百知教育 来源:学习课程 2019-10-15

一、简述


    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

                // 执行业务代码

           }

     }

}

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


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

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

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


三、Java内存模型


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


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


java培训


四、内存间交互操作


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


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


java培训


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在保证正确处理指令依赖、执行结果正确的前提下,会对一些指令进行顺序的优化,这样指令和实际的代码顺序就会有一些差别)会影响线程之间共享变量的使用。


举个例子:


java培训

5.png


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

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虚拟机》】


java培训学校:http://www.baizhiedu.com/java2019




上一篇:java培训班 | Java的变量及数据类型

下一篇:应届生去公司找个Java程序员的职位需要什么技能?

相关推荐

www.baizhiedu.com

有位老师想和您聊一聊

关闭

立即申请