JVM与多线程的原子性、可见性、有序性、重排序

1. 原子性(Atomicity):

一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

我们先来看看哪些是原子操作,哪些不是原子操作,先有一个直观的印象:

int  i = 7 ;  // 代码1

i++;  // 代码2

int k = i ; // 代码3

i = i +1 ; // 代码4

上面这4个代码中只有代码1是原子操作。

代码2:包含了三个操作。1.读取变量i的值;2.将变量i的值加1;3.将计算后的值再赋值给变量i。

代码3:包含了两个操作。1.读取变量i的值;2.将变量k的值赋值给变量k。

代码4:包含了三个操作。1.读取变量i的值;2.将变量i的值加1;3.将计算后的值再赋值给变量i。

注:实际编译成字节码后,这些代码的字节码条数跟我上面的操作数可能有出入,但为了更容易理解,并且这些操作已经总体上能说明问题,因此使用这些操作来分析。

上面这个例子只是简单的分析了几种常见的情况。具体到底层的指令(上文内存间操作提到的8个指令),由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的。如果应用场景需要一个更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块——synchronized关键字,因此在synchronized块之间的操作也具备原子性。

 

2. 可见性(Visibility):

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

我们先看下以下的例子,对可见性有一个直观的印象:

// 线程A执行的代码
int k = 0; //1
k = 5;     //2
// 线程B执行的代码
int j = k; //3

 

上面这个例子,如果线程A先执行,然后线程B再执行,j的值是多少了?

答案是无法确定。因为即使线程A已经把k的值更新为5,但是这个操作是在线程A的工作内存中完成的,工作内存所更新的变量并不会立即同步回主内存,因此线程B从主内存中得到的变量k的值是不确定的。这就是可见性问题,线程A对变量k修改了之后,线程B没有立即看到线程A修改的值。

Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。

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

 

3. 有序性(Ordering):

一个线程中的所有操作必须按照程序的顺序来执行。

我们先看下以下的例子,对有序性有一个直观的印象:

//例1
int k = 0; 
int j = 1  
k = 5; //代码1
j = 6; //代码2

 

按照有序性的规定,该例子中的代码1应该在代码2之前执行,但是实际上真的是这样吗?

答案是否定的,JVM并不保证上面这个代码1和代码2的执行顺序,因为这两行代码并没有数据依赖性,先执行哪一行代码,最终的执行结果都不会改变,因此,JVM可能会进行指令重排序。

//例2
int k = 1; // 代码1
int j = k; // 代码2

在单线程中,代码1的执行顺序会在代码2之前吗?

答案是肯定的,因为代码2依赖于代码1的执行结果,因此JVM不会对这两行代码进行指令重排序。

Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。

 

介绍完并发中3种重要的特性后,我们发现synchronized关键字在需要这3种特性的时候都可以作为其中一种的解决方案,看起来很“万能”。的确,大部分的并发控制操作都能使用synchronized来完成。synchronized的“万能”也间接造就了它被程序员滥用的局面,越“万能”的并发控制,通常会伴随着越大的性能影响。

 

4. 重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

目的:为了提高性能

4. 重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

目的:为了提高性能

重排序分为:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

重排序保证在单线程下不会改变执行结果,但在多线程下可能会改变执行结果。

 

如何禁止重排序?

可以通过插入内存屏障指令来禁止特定类型的处理器重排序。或者volatile关键字就有这种功能。

 

 

 

 
posted @ 2020-03-09 12:00  东哥的篮球鞋  阅读(197)  评论(0编辑  收藏  举报