volatile型变量的特殊规则

volatile是Java虚拟机提供的最轻量级的同步机制,当一个变量被定义成volatile后,它将具备两种特性,第一是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程改变了这个变量的值后,新值对于其他线程来说是可以立即得知的;第二个语义是禁止指令重排序,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

开发人员经常误解的一个描述:“volatile变量对于所有线程是立即可见的,对volatile变量的写操作都能立刻反应到其他线程之中,换句话说,volatile变量在各个线程中是一致的,所以基于volatile变量的运算在并发下是安全的”。这句话的论据部分并没有错,但是其论据不能得出“基于volatile变量的运算在并发下是安全的”这个结论。volatile型变量在各个线程的工作内存中不存在一致性问题,但是Java里面的运算并非原子运算,导致volatile变量的运算在并发下一样是不安全的。 象如下代码:

1
2
volatile int race =0 ;
race++

我们知道race++这个操作并不是原子运算,它会被编译成多条字节码指令,故此多线程计算时会出现结果不符合预期的情况。

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然需要通过加锁来保证原子性:

  • 1) 运算结果并不依赖变量的当前值,或者能够确保只有单一线程修改变量的值
  • 2) 变量不需要与其他的状态变量共同参与不变约束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Map configOptions;
char[] configText;
volatile boolean initialized = false;
//假设以下代码在线程A中执行
//模拟读取配置信息,当读取完成后
//将initialized设置为true 来通知其他线程配置可用
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText,configOptions);
initialized = true;
 
//假设以下代码在线程B中执行
while(!initialized){
sleep();
}
doSomethingWithConfig();

上述代码是一段伪代码,其中描述的场景十分常见,只是我们在处理配置文件时一般不会出现并发而已。如果定义initialized变量时没有使用volatile修饰,就可能由于指令重排序的优化,导致位于线程A中最后一句的代码”initialized=true”被提前执行,这样在线程B中使用配置信息的代码可能出现错误,而volatile关键字则可避免此类情况的发生。

再看看Java内存模型中对volatile变量定义的特殊规则,假定T表示一个线程,V和W分别表示两个volatile型变量,那么在进行read,load,use,assign,store和write操作时需要满足以下规则:

  • 1) 只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T对变量V执行load动作。线程T对变量V的use动作可以认为是与线程T对变量V的load和read动作相关联的,必须一起连续出现。(这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改后的值)
  • 2) 只有当线程T对变量V执行的前一个动作是assign的时候,线程T才能对变量V执行store动作;并且,只有当线程T对变量V执行的后一个动作是store的时候,线程T对变量V执行assign动作。线程T对变量V的assign动作可以认为是与线程T对变量V的store和write动作相关联的,必须一起连续出现。(这条规则要求在工作内存中,每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量V所做的修改)
  • 3) 假定动作A是线程T对变量V实施的use或assign动作,假定动作F是与A相关联的load或store动作,假定动作P是与动作F相应的对变量V的read或write动作;类似地,假定动作B是线程T对变量W实施的use或assign动作,假定动作G是与B相关联的load或store动作,假定动作Q是与动作G相应的对变量V的read或write动作。如果A先于B,那么P先于Q(这条规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)

原子性、可见性和有序性

  • 1) 原子性

    由Java内存模型直接保证的原子性变量操作包括read,load,use,assign,store,write这6个,我们大致可以认为基本数据类型的访问读写是具备原子性的(long和double除外)。

    如果需要更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,对应的字节码指令是monitorenter和monitorexit,对应到Java代码当中就是synchronized,但是至于moniorenter和monitorexit指令如何对应到lock和unlock操作的请参考《Java虚拟机规范》以及《Java语言规范》。

  • 2) 可见性

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

    volatile保证了多线程操作时变量的可见性。

    synchronized和final也能实现可见性,同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中”这条规则获得的,而final关键字的可见性是指:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去,那么在其他线程中就能看见final字段的值。(this引用逃逸是一件很危险的事情,其它线程可能访问到初始化了“初始化了一半”的对象)

  • 3) 有序性

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

    Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性。

先行发生原则(Happen-before)

先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A所产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值,发送了消息,调用了方法等。

Java内存模型中“天然的”先行发生关系:

  • 1) 程序次序规则(Program Order Rule)

    在同一个线程内,程序代码里写在前面的操作先行发生于写在后面的代码。准确地说,因该是控制流顺序而不是程序代码顺序,因为要考虑分支,循环等结构。

  • 2) 管程锁定规则(Monitor Lock Rule)

    对某个锁的unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,这里的“后面”是指时间上的先后顺序。也就是说,如果某个锁已经被lock了,那么只有它被unlock之后,其他线程才能lock该锁。表现在代码上,如果是某个同步方法,如果某个线程已经进入了该同步方法,只有当这个线程退出了该同步方法(unlock操作),别的线程才可以进入该同步方法。

  • 3) volatile变量规则(Volatile Variable Rule)

    对一个volatile变量的写操作先行发生于对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。也就是说,某个线程对volatile变量写入某个值后,能立即被其它线程读取到。

  • 4) 线程启动规则(Thread Start Rule)

    Thread对象的start方法先行发生于此线程的每个动作。

  • 5) 线程终止规则(Thread Termination Rule)

    线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等手段检测到线程是否已经终止运行。

  • 6) 线程中断规则(Thread Interruption Rule)

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

  • 7) 对象终结规则(Finalizer Rule)

    一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

  • 8) 传递性(Transitivity)

    如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

其中程序次序规则,管程锁定规则,volatile变量规则,传递性规则经常用来推断先行发生关系。

需要注意的是,先行发生关系和时间上的先后顺序基本没有太大的关系。

时间上的先后顺序不能得出先行发生关系,示例代码如下所示:

1
2
3
4
5
6
7
private int value=0;
public void setValue(int value){
this.value=value;
}
public int getValue(){
return value;
}

假设存在线程A和线程B,线程A先(时间上的先后)调用了”setValue(1)”,然后线程B调用了同一个对象的”getValue()”,那么线程B收到的返回值是不确定的,由于工作内存和主内存同步存在延迟,也由于可能存在重排序现象。 虽然时间上线程A的setValue()操作先于线程B的getValue()操作,但是并不能推断出线程A的setValue()操作先行发生于线程B的getValue()操作,如果有这种先行发生关系,那么可以推断出线程B的getValue()操作获得的值。

如果我们给getValue()方法和setValue()方法添加synchronized关键字,就能利用管程锁定规则推断出线程A的setValue操作先行发生于线程B的getValue操作,或者我们也可以将value定义为volatile变量,也能利用volatile变量规则推断出先行发生关系。

先行发生关系也不能推断出时间上的先后执行顺序,示例代码如下所示:

1
2
int i=1;
int j=2;

根据程序次序规则,我们可以推断出”int i=1”的操作先行发生于”int j=2”的操作,但是”int j=2”的代码完全有可能先被处理器执行(时间上的先后),这就是重排序,虚拟机规范是允许这种特性存在的,虚拟机可利用这种特性提高性能。

重排序

在同一个线程操作是顺序执行的(其实是按控制流执行),但是在某个线程看别的线程里的操作,则是乱序执行的。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class PossibleReording {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
 
public static void main(String[] args)
throws InterruptedException {
Thread one = new Thread() {
@Override
public void run() {
a = 1;
x = b;
}
};
Thread other = new Thread() {
@Override
public void run() {
b = 1;
y = a;
}
};
one.start();
other.start();
one.join();
other.join();
System.out.println("(" + x + "," + y + ")");
 
}
 
}

这段代码的执行结果是什么呢?我们先不考虑工作内存和主内存的同步问题,假设一种执行顺序,one线程先执行完,然后other线程再执行,那么执行的操作序列会是a=1, x=b, b=1, y=a; 此时的输出结果将是(0,1) 假设另一种执行顺序,other线程先执行,one线程后执行,那么执行的操作序列是b=1,y=a,a=1,x=b,此时的输出结果是(1,0),如果one线程和other线程交替执行,a=1,b=1,y=a,x=b,此时的输错结果是(1,1)。

但是让意外的是竟然还会存在这样一种输出结果(0,0),这种结果很难想象。这便是重排序导致的现象。一种可能的重排序后的执行顺序如下图所示:

线程A执行时本来应该先执行a=1,后执行x=b的,但是由于重排序的原因x=b先执行,a=1后执行。

这一节说的的先后是指时间上的先后,根据内存模型的程序次序规则,线程A里a=1还是先行发生于x=b的。

参考资料

《深入理解Java虚拟机》 第12章 Java内存模型与线程

《Java多线程设计模式》 附录B Java的内存模型。

转载 http://www.cloudchou.com/softdesign/post-637.html

Java内存模型(二)的更多相关文章

  1. java内存模型二

    数据依赖性 如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性.数据依赖分下列三种类型: 名称 代码示例 说明 写后读 a = 1;b = a; 写一个变量之 ...

  2. java内存模型(二)深入理解java内存模型的系列好文

    深入理解java内存模型(一)--基础 深入理解java内存模型(二)--重排序 深入理解java内存模型(三)--顺序一致性 深入理解java内存模型(四)--volatile 深入理解java内存 ...

  3. Java内存模型解惑--观深入理解Java内存模型系列文章有感(二)

    1.volatile关键字修饰的域的特性 当我们声明共享变量为volatile后,对这个变量的读/写将会很特别.理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用 ...

  4. 并发系列(二)----Java内存模型

    一 简介 在并发编程中,两个线程(A.B)同时操作一个普通变量的时候会出现线程A在操作变量时线程B也将变量操作了,此时线程A是无法感知变量发生变化的,造成变量改变错误.更据以上例子我们需要解决的问题就 ...

  5. Java并发(二):Java内存模型

    一.硬件内存架构 一个现代计算机通常由两个或者多个CPU.其中一些CPU还有多核.每个CPU在某一时刻运行一个线程是没有问题的.如果你的Java程序是多线程的,在你的Java程序中每个CPU上一个线程 ...

  6. Java并发编程(二):JAVA内存模型与同步规则

    一.Java内存模型(JMM) 它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式.一个线程如何和何时能看到其他线程共享变量的值,以及在 ...

  7. 并发与高并发(二)-JAVA内存模型

    一.java内存模型(JMM)-同步操作与规则 它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式.一个线程如何和何时能看到其他线程共享 ...

  8. java并发编程实战《二》java内存模型

    Java解决可见性和有序性问题:Java内存模型 什么是 Java 内存模型? Java 内存模型是个很复杂的规范,可以从不同的视角来解读,站在我们这些程序员的视角,本质上可以理解为, Java 内存 ...

  9. 二.GC相关之Java内存模型

    根据上节描述的问题,我们知道其最终原因是GC导致的.本节我们就先详细探讨下与GC息息相关的Java内存模型. 名词解释:变量,理解为java的基本类型.对象,理解为java new出来的实例. Jav ...

随机推荐

  1. Week4-作业1:阅读笔记与思考

    我在这三天时间里阅读了<构建之法>的第四章和第十七章,产生了一些疑问和深层次的思考. 第四章 问题1: 书中第68页提到“注释(包括所有源代码)应该只用ASCII字符,不要用中文或其他特殊 ...

  2. mysql连接数据库存报下面错误:ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)

    输入 mysql -u root 登录 mysql 的时候出现以下错误: ERROR 2002 (HY000): Can't connect to local MySQL server through ...

  3. ORACLE system表空间满

    解决方法:执行迁移命令,将AUD$表相关移到其它表空间中,也可以新建 一个审计 表空间 / MB DESC) ; alter table aud$ move tablespace SIEBELINDE ...

  4. 禁止直接访问ashx页面

      if (context.Request.ServerVariables["HTTP_REFERER"] == null)             {               ...

  5. LIS问题(DP解法)---poj1631

    题目链接:http://poj.org/problem?id=1631 这个题题目有些难看懂hhh,但实质就是求LIS--longest increasing sequence. 以下介绍LIS的解法 ...

  6. 未能加载文件或程序集“AjaxControlToolkit”或它的某一个依赖项

    对于这个问题,网上的解答都大同小异,最多的就是Bin文件夹下没有dll文件,引用路径问题.但我碰到的问题偏偏不是这个,而是没有一个人给出方法的问题.其实问题很简单,也很低级:IIS上发布网站的时候把整 ...

  7. js Function 函数

    函数 var abs = function (x) { if (x >= 0) { return x; } else { return -x; } }; 函数体内部的语句在执行时,一旦执行到re ...

  8. linux 下的 rsync 文件同步

    rsync是linux下的一款快速增量备份工具Remote Sync,是一款实现远程同步功能的软件,它在同步文件的同时,可以保持原来文件的权限.时间.软硬链接等附加信息.rsync是用 “rsync ...

  9. IIS 6.0/7.0/7.5、Nginx、Apache 等服务器解析漏洞总结

    IIS 6.0 1.目录解析:/xx.asp/xx.jpg  xx.jpg可替换为任意文本文件(e.g. xx.txt),文本内容为后门代码 IIS6.0 会将 xx.jpg 解析为 asp 文件. ...

  10. cookies,sessionStorage 和 localStorage 的区别

    请描述一下 cookies,sessionStorage 和 localStorage 的区别? sessionStorage 和 localStorage 是HTML5 Web Storage AP ...