Java并发编程--深入理解volatile关键字

前言

一个月以前就准备写篇关于volatile关键字的博客,一直没有动笔,期间看了大量的文章,发现一个小小volatile关键字竟然涉及JMM(Java memory model),JVM(Java virtual machine),Java多线程同步与安全各个方面的知识,写起了非常的困难,后面附带的参考文献仅仅是我看过文献的一部分。


Java memory model(Java内存模型)

在讲volatile关键字之前必须先了解一下Java内存模型,如果不了解Java内存模型volatile关键字无从讲起。先看看下面的图。

这里写图片描述

由上图可以看出来,在JMM中内存分为两部分一个是Main Memory(主内存)另一个是Working Memory(工作内存)

Main Memory(主内存):主要存放Variable,此处的变量(Variable)与Java编程中所说的变量有所区别,它包括了实例字段静态字段构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。 Java内存模型规定了所有的变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理硬件时的主内存名字一样,两者也可以互相类比,但此处仅是虚拟机内存的一部分)。

Working Memory(工作内存):每条线程还有自己的工作内存(Working Memory,可与处理器高速缓存类比),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、 赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。线程、 主内存、 工作内存三者的交互关系如下图所示。

这里写图片描述
也就是说Java每条线程所拥有的工作内存通过,sava和load指令与主内存进行交互。另外还需要了解的一点是:Java的一切指令操作都是基于栈的(stack),因此工作内存中又包含以下两个部分:

这里写图片描述

①操作数栈(Operand Stack)对应上图左边的stack,也常称为操作栈,它是一个后入先出栈。 当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。 例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。

举个例子,整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会将这两个int值出栈并相加,然后将相加的结果入栈。

②局部变量表(Local Variable Table)对应上图右边的variable是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。 在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。
关于JMM(Java内存模型)部分,只需要了解这么多。

内存间交互操作

关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、 如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了以下8种操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的、 不可再分的。

① lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。

②unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

③read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。

④load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

⑤use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。

⑥assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

⑦store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。

⑧write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

如果要把一个变量从主内存复制到工作内存,那就要顺序地执行read和load操作,如果要把变量从工作内存同步回主内存,就要顺序地执行store和write操作。 注意,Java内存模型只要求上述两个操作必须按顺序执行,而没有保证是连续执行。 也就是说,read与load之间、 store与write之间是可插入其他指令的,如对主内存中的变量a、 b进行访问时,一种可能出现顺序是read a、read b、load b、load a 关于内存间交互操作的更多细节请参考深入理解Java虚拟机第二版(393页)。

并发原则

①原子性(Atomicity):原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。由Java内存模型来直接保证的原子性变量操作包括read、 load、assign、 use、 store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的。

②可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

③有序性(Ordering):Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。 前半句是指“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

Java Volatile Keyword

先看一段英文解释:

The Java volatile keyword is used to mark a Java variable as “being stored in main memory”. More precisely that means, that every read of a volatile variable will be read from the computer’s main memory, and not from the CPU cache(working memory), and that every write to a volatile variable will be written to main memory, and not just to the CPU cache(working memory).

简单对上面的这段话做两点说明:
①线程读取一个被volatile修饰的变量时直接从main memory(主内存)中读取,而不是从working memory中读取。
②线程写对一个volatile变量进行写操作时,直接写入到main memory(主内存)中,而不仅仅是写到working memory中。

volatile确保可见性

先看下面的一个实例:

private  static boolean stop = false;

    public static void main(String[] args) {
        //线程1
        new Thread(){
            @Override
            public void run() {         
                    while(!stop)                    
                        System.out.println("is not stop!");                             
            }

        }.start();
        //线程2
        new Thread(){
            @Override
            public void run() {
                stop=true;
            }

        }.start();
    }

这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。

  下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

  那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

  但是用volatile修饰之后就变得不一样了:

  第一:使用volatile关键字会强制将修改的值立即写入主存;

  第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

  第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

  那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。那么线程1读取到的就是最新的正确的值。

volatile确保有序性

在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

  volatile关键字禁止指令重排序有两层意思:

  1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

  2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

  举个简单的例子:
  

//x、y为非volatile变量
//flag为volatile变量

x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

  并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

volatile不能保证原子性

为了说明volatile不保证原子性先举一个经典的例子,这个例子来自于深入理解Java虚拟机第二版(395页)


public class VolatileTest{

    public static volatile int race=0public static void increase(){
        race++;
    }

    private static final int THREADS_COUNT=20public static void main(String[]args){

        Thread[]threads=new Thread[THREADS_COUNT];

        forint i=0;i<THREADS_COUNT;i++){

            threads[i]=new Thread(new Runnable(){

            @Override
            public void run(){
                forint i=0;i<10000;i++){
                        increase();
                }
        }
        );
        threads[i].start();
    }
    //等待所有累加线程都结束
    while(Thread.activeCount()>1)
        Thread.yield();
        System.out.println(race);
    }
}

这段代码发起了20个线程,每个线程对race变量进行10000次自增操作,如果这段代码能够正确并发的话,最后输出的结果应该是200000。 读者运行完这段代码之后,并不会获得期望的结果,而且会发现每次运行程序,输出的结果都不一样,都是一个小于200000的数字。
先来看看上面代码中increase()方法的字节码,然后对照着JMM来分析一下为什么会出现上面程序运行的结果。

public static void increase();
Code:
Stack=2,Locals=0,Args_size=0
0:getstatic#13//从主存中直接获取race的值放入到操作栈中。
3:iconst_1  //将常数1放入操作栈中,
4:iadd  //获取操作栈中的两个值相加
5:putstatic#13//将操作栈中的race的值直接放入到主存中。
8return
LineNumberTable:
line 140
line 158

对于上面的字节码,给出一个运行图:

这里写图片描述

将Race=1放入到stack中,将常量1放入到stack中:
这里写图片描述

将stack中的两个数相加,并将结果放回到Main Memory中:
这里写图片描述

有上面的过程图可知,当执行将Race=1放入到stack中,将常量1放入到stack中时,其他线程可能抢占jvm此时还未执行后面的加法操作,比如说两个线程同时执行了前面的两步,将race=1放到了stack中,然后又执行加操作,此时虽然执行了两次加操作,race的值却是2,所以综合可知volatile并不能保证原子性。

参考文献

深入理解Java虚拟机第二版
http://www.cnblogs.com/dolphin0520/p/3920373.html
http://jeremymanson.blogspot.sg/2008/11/what-volatile-means-in-java.html
https://stackoverflow.com/questions/4885570/what-does-volatile-mean-in-java
http://tutorials.jenkov.com/java-concurrency/volatile.html

©️2020 CSDN 皮肤主题: Age of Ai 设计师: meimeiellie 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值