本文共 3085 字,大约阅读时间需要 10 分钟。
Java虚拟机试图定义一种内存模型(Java Memory Model, JMM)来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
主内存与工作内存
Java内存模型规定了所有的变量都存储在主内存中,线程的工作内存保存了该线程使用到的变量的主存拷贝副本,线程对变量的所有操作(读取,赋值)都必须在工作内存中进行,而不能直接读写主存中的变量,线程间变量值的传递均需要主内存来完成,线程,主存,工作内存的交互关系如下: 举个简单的例子:i = 10
执行线程必须在自己的工作内存对i的副本进行赋值,然后才能回写到主存中。
内存间交互操作
Java定义了8种内存操作:Java内存模型还规定了执行上述8种操作时需要满足的规则:
原子性,可见性,有序性
Java的内存模型是围绕并发中如何处理原子性,可见性和有序性这三个特征建立的。原子性
原子性指一个或者多个操作要么都不执行,要么全部被执行,也就是不能被终端。Java内存模型来保证的原子操作有read,load,assign,use,store和write。基本所有的基本数据类型的读写操作可以认为是原子的。 如果一个应用场景需要更大范围的原子操作,java内存模型提供了lock和unlock操作,反映到java代码就是synchronized。可见性
可见性指当以个线程修改了共享变量的值,其他线程能够立即得到这个修改。Java中是通过volatile关键字来实现可见性的,volatile保证变量在修改后立即会刷新到主存中。 除了volatile之外,Java还提供了两个关键字实现可见性,synchronized和final。同步块的可见性由“对一个变量执行unlock前,必须先把此变量同步回主存(执行store,write操作)”这条规则获得的,而final关键字的可见性:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this引用传递出去,那么其他线程就可以看见final字段的值。有序性
有序性即程序执行的顺序按照代码的先后顺序执行。看下面的代码:
int i = 0; boolean flag = false; i = 1; //语句1 flag = true; //语句2
上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。
下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。 Java内存模型中允许编译器和处理器进行指令重排,但是重排不会影响影响单线程的执行,确会影响到多线程并发执行的正确性。看下面的例子://线程1 context = loadContext(); //语句1 inited = true; //语句2 //线程2 while (! inited) { sleep(); } doSomethingwithconfig();
上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。
Java内存模型中存在着一些“天然”的先行发生关系,也就是所谓的happens-before原则。
程序次序原则
一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。管程锁定规则
一个unlock操作先行发生于同一个锁的lock操作。volatile变量规则
对一个变量的写操作先行发生于后面对这个变量的读操作。线程启动规则
Thread对象的start方法先行发生于此线程的每一个动作。线程终止规则
线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等手段检测到线程已经终止执行。线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。对象终结规则
一个对象的初始化先行发生于它的finalize()方法。传递性
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则操作A先行发生于操作C。本文中主要从理论的角度讲解了Java内存模型,常用的内存操作,已经内存模型相关的特性。