为什么要了解Java的内存模型?Java的内存模型是程序运行的基础知识,对于我们理解Java并发编程有一定的帮助,甚至一些并发知识的底层实现原理也是基于Java的内存模型来实现的。
计算机系统硬件与执行效率的一致性“让计算机并发执行若干个并发任务”与“更充分的利用计算机的处理器效能”之间的因果关系相对复杂,这种复杂性的来源是绝大多数的运算任务不可能只是靠处理器的“计算”就能完成的。处理器至少要与内存进行交互,如读取运算数据、存储运算结果等,这个IO *** 作就很难消除的(无法仅仅靠寄存器来完成所有的运算任务)。
而由于处理器的运算速度和存储设备的存取速度有几个数量级的差别,所以现代计算机不得不加入一层或多层读写速度接近与处理器速度的高速缓存(Cache)来作为内存和处理器之间的缓冲。将运算所需要使用的数据复制到缓存中,运算结束后再将结果同步回内存中,这样处理器就无需等待缓慢的内存读写了。
而缓存的引入虽然解决了内存存储设备存取和CPU处理器执行效率上的矛盾,但缓存的引入,也带来了额外的问题,缓存一致性问题。尤其是在多处理器系统中,每个处理器单元都有自己的高速缓存,而它们又同时共享一块内存,数据不一致的问题就需要得到解决,各个处理器就需要遵循一定的缓存一致性协议。
处理器、高速缓存、主内存之间的交互关系如下图:
为了提升处理器的效能有两种手段,处理上面介绍的加入高速缓存之外,另一个手段是处理器可能输入的代码进行乱序执行优化,处理器会在计算之后,将乱序的执行结果重组、保证该结果与顺序执行的结果一致。Java虚拟机的即时编译器中也有指令重排优化。
java的内存模型的主要目的是定义程序中各种变量的访问规则,即关注虚拟机如何把变量存储到内存和如何从内存中取出变量值的底层细节。变量包括实例字段、静态字段和构成数组的对象元素,不包括局部变量和函数参数(局部变量和函数参数是属于线程独占的)。
java的内存模型规定了所有变量都存储在主内存(main memory)中,每条线程都有自己的工作内存(working memory)。此处的主内存可以类比物理机的主内存、工作内存可类比物理机的高速缓存部分。
线程的工作内存保存了该线程使用变量的主内存副本,线程对所有变量的 *** 作(读取、赋值等)都必须在工作内存中完成,而不能直接读写主内存数据。
线程、主内存、工作内存三种的交互关系图如下:
另外,不同的线程之间是无法直接访问对方的工作内存中的变量,线程间的变量传递均需要通过主内存来完成。则必须至少经历如下两步才能完成线程间的通信。
1)线程A将本地内存更新过的共享变量x=1刷新到主内存中;
2)线程B到主内存读取已经被线程A更新过的共享变量x;
试想:线程A、线程B在同时对主内存的共享变量进行读取和写入 *** 作时,则很有可能出现数据不一致的问题,脏读、脏写都有可能发生。而基于这样的java内存模型,并发数据不一致的问题则很有可能产生,这也是并发处理问题的本质。
这一节我们更详细的了解主内存和工作内存之间的交互协议。Java的内存模型中定义了8种 *** 作来完成主内存和工作内存之间的交互,即如何完成一个变量从主内存拷贝到工作内存、如何从工作内存同步回主内存。
java虚拟机实现时必须保证下面提及的每一种 *** 作都是原子的,不可再分的(对于double和long类型的变量来说,有所例外),如下是8种jvm定义的 *** 作规范:
- lock(锁定):作用与主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把一个变量从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于工作内存变量,它把read *** 作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存变量,把工作内存中的一个变量值传递给执行引擎;每当虚拟机遇到一个需要使用变量值的字节码指令时将会执行这个 *** 作。
- assign(赋值):作用与工作内存变量,它把一个从执行引擎接收到的值赋值给工作内存变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个 *** 作。
- store(存储):作用与工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write *** 作。
- write(写入):作用于主内存的变量,他把store *** 作从工作内存中得到的变量值赋值给主内存中的变量。
JMM还规定了在执行上述8种基本 *** 作时,必须满足以下规则:
- 如果把一个变量从主内存中复制到工作内存,就需要按顺序地执行read和load *** 作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write *** 作。但Java内存模型只要求上述 *** 作必须按顺序执行,而没有保证必须是连续执行。
- 不允许read和load、store和write *** 作之一单独出现。
- 不允许一个线程丢弃它的最近assign的 *** 作,即变量在工作内存中改变了之后必须同步到主内存中。
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store *** 作之前,必须先执行过assign和load *** 作。
- 一个变量在同一时刻只允许一条线程对其进行lock *** 作,但lock *** 作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock *** 作,变量才能被解锁。lock和unlock必须成对出现。
- 如果对一个变量执行lock *** 作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign *** 作初始化变量的值。
- 如果一个变量事先没有被lock *** 作锁定,则不允许对它执行unlock *** 作;也不允许去unlock一个被其他线程锁定的变量。
- 对一个变量执行unlock *** 作之前,必须先把此变量同步到主内存中(执行store和write *** 作)。
8种 *** 作以及在8中 *** 作上的规则限定能够准确的描述出java程序中哪些内存访问 *** 作在并发下才是安全的。但是这种规则判断相对复杂,另一种等效的判定原则是先行发生原则,用来确定一个 *** 作在并发环境下是否安全。
Java内存模型解决什么问题?Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的。
原子性由Java内存模型来直接保证原子性变量 *** 作包括read、load、assign、use、store、write,可以认为基本数据类型的访问和读写都具备原子性的(例外double、long的非原子性协定)。
通常应用场景中需要更大范围的原子性保证,java内存模型中提供的lock、unlock *** 作可满足需求,虚拟机未把lock和unlock *** 作直接开放给用户,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式的使用这两个 *** 作。这两个字节码指令反映到java代码中就是同步块-synchronized关键字,因此在synchronized块直接的 *** 作也是具备原子性的。(这里我们也看到synchronized实现同步锁的原理,是通过上述两个字节码monitorenter、monitorexit指令来完成)。
可见性是指当一个线程修改了共享变量的值时,其它线程能够立即得知这个修改。java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量或是volatile变量都是如此。普通变量和volatile变量的区别是,volatile的特殊规则(单独文章篇幅分析)保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此我们可以说volatile保证了多线程 *** 作变量的可践行,而普通变量不能保证这一点。
除了volatile之外,java还有两个关键字可以实现可见性,synchronized和final。
synchronized同步块的可见性是由“对一个变量执行unlock之前,必须先把此变量同步回主内存中(执行store、write *** 作)”这条规则获得的。被synchronized包裹着同步代码会被lock,此时其他线程无法进入,当其他线程内你各个进入时,必然执行了unlock,此时上述规则中已经将变量值同步到主内存,从而保证了可见性。
final关键字的可见性是指:被final修饰的字段在构造器中一旦被初始化完成,并且构造器中没有把"this”的引用传递出去,那么在其他线程就能看见final字段的值。
java程序中天然的有序性可以总结成一句话:“如果在本线程内观察,所有的 *** 作都是有序;如果在一个线程中观察另一个线程,所有 *** 作都是无序的”。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和”工作内存和主内存同步延迟“现象。在同一个线程中,即使指令重排序,但指令重排序的规则是不影响最终执行结果。在多个线程间,所有非原子 *** 作都是无序的,一个线程可能由于另一个线程对本地工作内存的 *** 作而延迟同步到主内存,而获得共享变量错误的数据。
实现有序性的手段,在Java语言中提供了volatile和synchronized两个关键字来保证线程之间 *** 作的有序性。volatile关键字本身就包含了禁止指令重排序语义,而synchronized关键字则是由“一个变量在同一时刻只允许一条线程对其进行lock *** 作”这条规则获得的。这个规则保证了,持有同一个锁的多个线程只能串行的进入。
在解决可见性、有序性问题上volatile关键字可以作为解决手段。原子性、可见性、有序性这三类问题通过synchronized关键字得到解决。当然synchronized关键字也有性能问题,虽然随着java版本的升级,以及jvm对锁的优化,这种性能问题也随之降低。
工作内存和主内存同步延迟如何产生的工作内存和主内存同步延迟如何产生的?
- 工作内存中的值已经更新了,但是还没有同步到主存中,因为更新工作内存编号和同步主存不是一个原子 *** 作。
- 工作内存变量何时加载主存变量呢?
参考:Java工作内存与主内存同步时机
java中有一个“先行发生”(happens-before)原则,基于这个原则可以判断是否发生数据竞争,是判断是否线程安全的重要的手段。依赖这个规则我们可以通过几条简单的规则一揽子解决并发环境下两个 *** 作之间是否存在冲突的所有问题,而不陷入java内存模型的复杂中。
先行发生原则是java内存模型中定义了两项 *** 作之间的偏序关系,比如说 *** 作A先行发生于 *** 作B,其实就是说发生 *** 作B之前 *** 作A产生的影响能被 *** 作B观察到。“影响”包括修改了内存中共享变量的值发送了消息、调用了方法等。
这句话不难理解,但它意味着什么呢?我们可以举个例子来说明一下,如下伪代码:
//以下 *** 作在线程A中执行
i=1;
//以下 *** 作在线程B中执行
j=i;
//以下 *** 作在线程C中执行
i=2;
假设线程A中的 *** 作“i=1”先行发生于线程B的 *** 作“j=i”,那么可以确定在线程B的 *** 作执行后,变量j的值一定等于1,得出这个结论的依据有两个:一是根据先行发生原则,“i=1”的结果可以被观察到;二是线程C还没“登场”,线程A *** 作结束之后没有其他线程会修改变量i的值。 现在再来考虑线程C,我们依然保持线程A和线程B之间的先行发生关系,而线程C出现在线程A和线程B的 *** 作之间,但是线程C与线程B没有先行发生关系,那j的值会是多少呢?答案是不确定!1和2都有可能,因为线程C对变量i的影响可能会被线程B观察到,也可能不会,这时候线程B就存在读取到过期数据的风险,不具备多线程安全性。
3.4.1Java内存模型中的先行发生关系下面是Java内存模型下一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。 如果两个 *** 作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序:
- 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的 *** 作先行发生于书写在后面的 *** 作。 准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、 循环等结构。
- 管程锁定规则(Monitor Lock Rule):一个unlock *** 作先行发生于后面对同一个锁的lock *** 作。 这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
- volatile变量规则(Volatile Variable Rule):对一个volatile变量的写 *** 作先行发生于后面对这个变量的读 *** 作,这里的“后面”同样是指时间上的先后顺序。
- 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
- 线程终止规则(Thread Termination Rule):线程中的所有 *** 作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、 Thread.isAlive()的返回值等手段检测到线程已经终止执行。
- 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
- 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
- 传递性(Transitivity):如果 *** 作A先行发生于 *** 作B, *** 作B先行发生于 *** 作C,那就可以得出 *** 作A先行发生于 *** 作C的结论。
Java语言无须任何同步手段保障就能成立的先行发生规则就只有上面这些了,笔者演示一下如何使用这些规则去判定 *** 作间是否具备顺序性,对于读写共享变量的 *** 作来说,就是线程是否安全,读者还可以从下面这个例子中感受一下“时间上的先后顺序”与“先行发生”之间有什么不同:
private int value=0;
pubilc void setValue(int value){
this.value=value;
}
public int getValue(){
return value;
}
以上显示的是一组再普通不过的getter/setter方法,假设存在线程A和B,线程A先(时间上的先后)调用了“setValue(1)”,然后线程B调用了同一个对象的“getValue()”,那么线程B收到的返回值是什么?
我们依次分析一下先行发生原则中的各项规则,由于两个方法分别由线程A和线程B调用,不在一个线程中,所以程序次序规则在这里不适用;由于没有同步块,自然就不会发生lock和unlock *** 作,所以管程锁定规则不适用;由于value变量没有被volatile关键字修饰,所以volatile变量规则不适用;后面的线程启动、 终止、 中断规则和对象终结规则也和这里完全没有关系。 因为没有一个适用的先行发生规则,所以最后一条传递性也无从谈起,因此我们可以判定尽管线程A在 *** 作时间上先于线程B,但是无法确定线程B中“getValue()”方法的返回结果,换句话说,这里面的 *** 作不是线程安全的。
那怎么修复这个问题呢?我们至少有两种比较简单的方案可以选择:要么把getter/setter方法都定义为synchronized方法,这样就可以套用管程锁定规则;要么把value定义为volatile变量,由于setter方法对value的修改不依赖value的原值,满足volatile关键字使用场景,这样就可以套用volatile变量规则来实现先行发生关系。
通过上面的例子,我们可以得出结论:一个 *** 作“时间上的先发生”不代表这个 *** 作会是“先行发生”,那如果一个 *** 作“先行发生”是否就能推导出这个 *** 作必定是“时间上的先发生”呢?很遗憾,这个推论也是不成立的,一个典型的例子就是多次提到的“指令重排序”,演示例子如下代码所示:
//以下 *** 作在同一个线程中执行
int i=1;
int j=2;
以上代码的两条赋值语句在同一个线程之中,根据程序次序规则,“int i=1”的 *** 作先行发生于“int j=2”,但是“int j=2”的代码完全可能先被处理器执行,这并不影响先行发生原则的正确性,因为我们在这条线程之中没有办法感知到这点。
上面两个例子综合起来证明了一个结论:时间先后顺序与先行发生原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。
深入理解Java的内存模型与线程并发问题
先行发生原则(happens-before)介绍
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)