Java并发
是基于共享内存模型实现的。学习并深入地理解__Java内存模型__,有助于开发人员了解Java的线程间通信机制原理,从而实现安全且高效的多线程功能。
计算机在执行程序时,每条指令都是在__CPU__中执行的,而执行指令过程中,势必涉及到对主存中数据的读取和写入。由于__CPU__的处理速度相比对内存数据的访问速度快很多,如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在__CPU__里面就有了高速缓存。
然而引入高速缓存带来方便的同时,也带来了缓存一致性的问题。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自缓存数据不一致的问题。解决方法是缓存一致性协议(如Intel 的MESI协议)。
MESI协议保证了每个缓存中使用的共享变量的副本是一致的。当CPU写数据时,如果发现操作的变量是共享变量,会发出信号通知其他CPU将该变量的缓存行置为无效状态。因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
除了增加高速缓存之外,为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致,因此,如果存在一个计算任务依赖另外一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。
为了方便理解Java内存模型,我们可以抽象地认为,所有变量都存储在主内存中(Main Memory),每个线程都拥有一个私有的工作内存(Working Memory),保存了该线程已访问的变量副本。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。
假设__线程A__要向__线程B__发消息,__线程A__需要先在自己的工作内存中更新变量,再将变量同步到主内存中,随后__线程B__再去主内存中读取A更新过的变量。因而可以看出,JMM通过控制主内存与每个线程的本地内存之间的交互,提供内存可见性保证。
Java内存模型定义的8个操作指令来进行内存之间的交互,如下:
read load use assign store write lock unlock
工作内存和主内存间的指令操作交互,如下图所示:
在没有正确同步的情况下,即使要推断最简单的并发程序的行为也很困难。代码如下:
public class PossibleReordering { static int x = 0, y = 0; static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException { Thread one = new Thread(new Runnable() { public void run() { a = 1; x = b; } }); Thread other = new Thread(new Runnable() { public void run() { b = 1; y = a; } }); one.start(); other.start(); one.join();other.join(); System.out.println(“(” + x + “,” + y + “)”); } 复制代码
很容易想象 PossibleReordering
的输出结果是(1,0)或(0,1)或(1:1)的,__但奇怪的是__还可以输出(0,0)。
由于每个线程中的各个操作之间不存在数据依赖性,因此这些操作可以乱序执行。下图给出了一种可能由重排序导致的交替执行方式,在这种情况中会输出(0,0)。
Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序,如图所示:
在执行程序时,为了提高性能,编译器和处理器会对指令做重排序。重排序分3种类型:
为了提高执行性能,JMM允许编译器和处理器对指令进行重排序。但是Java语言保证了操作间具有一定的有序性,概括起来就是先行发生原则(happens-before)。也就是说, 如果两个操作的关系无法被happens-before原则推导,则无法保证它们的顺序性,有可能发生重排序 。happens-before原则包括:
实际上,这些规则是由编译器重排序规则和处理器内存屏障插入策略来实现的。
内存屏障是一条 CPU指令 ,用于控制特定条件下的重排序和内存可见性问题。即任何指令都不能与内存屏障指令重排序。
从上面可以看到不同的处理器架构对重排序的支持也是不一样(其它处理器架构暂不罗列),所以不同的平台JMM的内存屏障施加也略有不同,具体来说,比如 X86 对Load1Load2不支持重排序,那么你就没有必要施加 LoadLoad
屏障。
volatile
关键字用来保证数据可见性,防止指令重排的效果。包括JUC里AQS Lock的底层实现也是基于 volatitle
来实现。
final修饰的称作域,对于final域,编译器和处理器要遵守两个重排序规则
写规则 :在构造函数内对一个final域的写入,与随后把这个被构造的对象的引用赋值给一个引用变量,这两个操作不可重排序。
JMM禁止编译器把final域的写重排序到构造函数之外, 编译器会在final域写入的后面插入 StoreStore
屏障,该规则可以保证在对象引用为任意线程可见之前,对象的final域已经被正确初始化,而普通域无法保障。
读规则:初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作。编译器会在读final域操作的前面插入一个LoadLoad屏障。