Java内存模型(二)

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

时间: 2024-10-27 00:48:14

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

java内存模型二

并发编程模型的分类 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体).通信是指线程之间以何种机制来交换信息.在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递. 在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信.在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信. 同步是指程序用于控制不同线程之间操作发生相对顺序的机制.在共享内存并发

深入java内存模型(二) volatile详解

对于volatile修饰符,我们应该不会陌生,在多线程中使用常见,比如多个线程想用一个全局变量作为标识符,或者一个共享变量,我们都会给该变量加上一个volatile的修饰符.volatile用中文解释是易变的,不稳定的.说明该变量会被多个线程访问并可能修改.那么jvm是怎样发挥volatile关键字的作用,如何实现的呢? 上一篇深入java内存模型中解释了jvm中的重排序以及四种内存屏障等.jvm总是会以一些易懂,使用方便的方式来实现相关功能.比如垃圾回收器,对于内存的申请与释放时一个令人头疼的

《java并发编程的艺术》读书笔记-第三章Java内存模型(二)

一概述 本文属于<java并发编程的艺术>读书笔记系列,第三章java内存模型第二部分. 二final的内存语义 final在Java中是一个保留的关键字,可以声明成员变量.方法.类以及本地变量.可以参照之前整理的关键字final.这里作者主要介绍final域的内存语义. 对于final域,编译器和处理器要遵守两个重排序规则: 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序. 初次读一个包含final域的对象的引用,与随后初次读这

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

深入理解java内存模型(一)--基础 深入理解java内存模型(二)--重排序 深入理解java内存模型(三)--顺序一致性 深入理解java内存模型(四)--volatile 深入理解java内存模型(五)--锁 深入理解java内存模型(六)--final 深入理解java内存模型(七)--总结 原文地址:https://www.cnblogs.com/jinggod/p/8495490.html

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

一 简介 在并发编程中,两个线程(A.B)同时操作一个普通变量的时候会出现线程A在操作变量时线程B也将变量操作了,此时线程A是无法感知变量发生变化的,造成变量改变错误.更据以上例子我们需要解决的问题就是线程之间的通信以及同步.表在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递.Java并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信对程序员玩完全透明. 二 Java内存模型的抽象结构 Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一

二.GC相关之Java内存模型

根据上节描述的问题,我们知道其最终原因是GC导致的.本节我们就先详细探讨下与GC息息相关的Java内存模型. 名词解释:变量,理解为java的基本类型.对象,理解为java new出来的实例. Java程序运行在JRE(Java Runtime Environment)中,JRE包括JAVA API和JVM(Java Virtual Machine). Java原文件编译后得到Java Byte Code(.class)文件,JRE通过classloader将Java byte code文件加载

Java-JUC(二):volatile对Java内存模型中的可见性、原子性、有序性影响

Java内存模型 Java内存模型-可见性 Java内存模型-原子性 Java内存模型-有序性 volatile-是否具有可见性? volatile-是否具有原子性? volatile-是否具有有序性? 原文地址:https://www.cnblogs.com/yy3b2007com/p/8901518.html

JVM学习记录-Java内存模型(二)

对于volatile型变量的特殊规则 关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制. 在处理多线程数据竞争问题时,不仅仅是可以使用synchronized关键字来实现,使用volatile也可以实现. Java内存模型对volatitle专门定义了一些特殊的访问规则,当一个变量被定义为volatile时,它将具备以下两个特性: 第一个是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的.而普通变量不能做到这

十二、深入理解Java内存模型

深入理解Java内存模型 [1]CPU和缓存的一致性 ? 我们应该都知道,计算机在执行程序的时候,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道.而计算机上面的数据,是存放在主存当中的,也就是计算机的物理内存啦. ? 刚开始,还相安无事的,但是随着CPU技术的发展,CPU的执行速度越来越快.而由于内存的技术并没有太大的变化,所以从内存中读取和写入数据的过程和CPU的执行速度比起来差距就会越来越大,这就导致CPU每次操作内存都要耗费很多等待时间. ? 所以,人们想出来了一个好的