资讯专栏INFORMATION COLUMN

java内存模型

2i18ns / 2800人阅读

摘要:顺序一致性内存模型有两大特性一个线程中所有操作必须按照程序的顺序执行。这里的同步包括对常用同步原语的正确使用通过以下程序说明与顺序一致性两种内存模型的对比顺序一致性模型中所有操作完全按程序的顺序串行执行。

java内存模型 java内存模型基础 happen-before模型

JSR-133使用happen-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happen-before关系。在这里两个操作可以在一个线程之内,也可以在不同的线程之间。与程序员相关的happen-before规则如下:

程序顺序一致性:一个线程中的每个操作,happen-before于该线程中的任意后续操作。(不要扣字,若两操作没有依赖关系,且变更操作顺序不影响结果,此时顺序可以变更。与程序一致性规则不冲突)

监视器锁规则: 对一个锁的解锁,happen-before于随后对这个锁的加锁。

volatile变量规则:对一个volatile域的写操作,happen-before于任意后续对这个volatile域的读。

传递性:如果A happen-before B且B happen-before C ,那么A happen-before C。

start()规则:如果线程A执行操作ThreadB.start(),那么A线程的ThreadB.start()操作happen-before于B中的任何操作。

join()规则:如果ThreadA 执行操作ThreadB.join()并成功返回,那么ThreadB中的任意操作happen-before于ThreadA从ThreadB.join()操作成功返回。

两个操作间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行。happens-before仅仅要求前一个操作对后一个操作可见。

重排序

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

数据相互信赖的两个操作不能进行重排序

as-if-serial语言,不管怎么得排序(编译器和处理器为了提高并行度),单线程程序执行的结果不能改变。

重排序对多线程的影响,代码如下:

</>复制代码

  1. /**
  2. * 操作1 操作2 之间无依赖关系, 可以进行重排序
  3. * 操作3 操作4 之间无依赖关系, 可以进行重排序
  4. * Thread B 中并不一定能看到Thread A 中对共享变量的写入。此时重排序操作破坏多线程语义
  5. **/
  6. class ReorderExample{
  7. int a = 0;
  8. boolean flag = false;
  9. public void writer(){ //Thread A
  10. a = 1; //1
  11. flag = true; //2
  12. }
  13. public void reader(){ //Thread B
  14. if(flag){ //3
  15. int i = a * a; //4
  16. }
  17. }
  18. }
顺序一致性

顺序一致性内存模型是一个理论参考模型,在设计的时候,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参照。顺序一致性内存模型有两大特性:

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

(不管程序是否同步)所有线程都只能看到单一的操作执行顺序,在顺序一致性内存模型中,每个操作都必须原子执行且立即对所有线程可见。

JMM对正确同步的多线程程序的内存一致性做了如下保证
如果程序是正确同步的,程序的执行将具有顺序一致性(Sequentially Consistent)--即程序的执行结果与该程序在顺序一致性内存模型中执行结果相同。这里的同步包括对常用同步原语(Synchronized,volatile,final)的正确使用

通过以下程序说明JMM与顺序一致性 两种内存模型的对比

</>复制代码

  1. /**
  2. *顺序一致性模型中,所有操作完全按程序的顺序串行执行。而在JMM中,临界区内的代码
  3. *可以重排序(但JMM不允许临界区内的代码“逸出”到临界区之外,那样会破坏监视器的语
  4. *义)。JMM会在退出临界区和进入临界区这两个关键时间点做一些特别处理,使得线程在这两
  5. *个时间点具有与顺序一致性模型相同的内存视图,虽然线程A在临界
  6. *区内做了重排序,但由于监视器互斥执行的特性,这里的线程B根本无法“观察”到线程A在临
  7. *界区内的重排序。这种重排序既提高了执行效率,又没有改变程序的执行结果。
  8. *
  9. */
  10. class SynchronizedExample {
  11. int a = 0;
  12. boolean flag = false;
  13. public synchronized void writer() { // 获取锁
  14. a = 1;
  15. flag = true;
  16. } // 释放锁
  17. public synchronized void reader() { // 获取锁
  18. if (flag) {
  19. int i = a;
  20. ......
  21. } // 释放锁
  22. }

未同步程序在JMM中的执行时,整体上是无序的,其执行结果无法预知。

顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行(比如上面正确同步的多线程程序在临界区内的重排序).

顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。

JMM不保证对64位的long型和double型变量的写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性。

volatile内存语义 volatile特性

可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

原子性: 对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

</>复制代码

  1. class VolatileFeaturesExample {
  2. volatile long vl = 0L;
  3. public void set(long l) {
  4. vl = l;
  5. }
  6. public void getAndIncrement () { //复合volatile读写,不具有线程安全
  7. vl++;
  8. }
  9. public long get() {
  10. return vl;
  11. }
  12. }
volatile 写-读建立的happen-before关系

示例代码如下:

根据程序次序规则,1 happen-before 2, 3 happen-before 4.(疑问1:1与2,3与4两操作没有依赖,为何不能重排,若发生重排,结果有可能会发生变化)

根据volatile规则 2 happen-before 3

根据happen-before传递性规则,1 happen-before 4.

</>复制代码

  1. class VolatileExample {
  2. int a = 0;
  3. volatile boolean flag = false;
  4. public void writer() {
  5. a = 1; // 1
  6. flag = true; // 2
  7. }
  8. public void reader() {
  9. if (flag) { // 3
  10. int i = a; // 4
  11. ......
  12. }
  13. }
volatile 写-读内存原语

volatile写操作,JMM会把线程对应的本地内存中的共享变量值刷新到主内存。

volatile读操作,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

volatile 内存语义的实现

为了实现volatile内存语义,JMM分分别限制这两种重排序类型,下图JMM针对编译器制定的volatile重排序规则表

这个重排序规则解释了疑问1。实现:是通过编译器生成字节码时,插入内存屏障来达到这个限制,在此处不作展开,有兴趣可以查阅相关资料

JSR-133增强volatile的内存原语

在JSR-133之前的旧Java内存模型中,虽然不允许volatile变量之间重排序,但旧的Java内存模型允许volatile变量与普通变量重排序
因此,在旧的内存模型中,volatile的写-读没有锁的释放-获所具有的内存语义。为了提供一种比锁更轻量级的线程之间通信的机制,JSR-133专家组决定增强volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义。从编译器重排序规则和处理器内存屏障插入策略来看,只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语义,这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止。

锁的内存语义 锁的释放与获取内存原语

当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

</>复制代码

  1. /**
  2. *
  3. */
  4. class MonitorExample {
  5. int a = 0;
  6. public synchronized void writer() { // 1
  7. a++; // 2
  8. } // 3
  9. public synchronized void reader() { // 4
  10. int i = a; // 5
  11. ......
  12. } // 6
  13. }

假设线程A执行writer()方法,随后线程B执行reader()方法。根据happens-before规则,这个过程包含的happens-before关系可以分为3类。

根据程序次序规则,1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happens-before 6。

根据监视器锁规则,3 happens-before 4。

根据happens-before的传递性,2 happens-before 5。

锁的释放与获取的内存语义

线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中

线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量

final 域内存语义

final 域,编译器与处理器要遵守两个重排序规则

在构造函数内对一个final域的写入,与随后把这个被构造对像的引用赋值给一个引用变量,这两个操作之间不能重排序

初次读一个包含final域的对象引用,与随后初次读这个final域,这两个操作之间不能重排序(有点扰,eg:obj,obj.j的关系)

下面的示例代码,说明这两个规则

</>复制代码

  1. /**
  2. *
  3. */
  4. public class FinalExample{
  5. int i;
  6. final int j;
  7. static FinalExample obj;
  8. static FinalExample(){
  9. i = 1;
  10. j = 2;
  11. }
  12. public static void writer(){
  13. obj = new FinalExample();
  14. }
  15. public static void reader(){
  16. FinalExample object = obj;
  17. int a = obj.i;
  18. int b = obj.j;
  19. }
  20. }
写final域重排序规则

MM禁止编译器把final域的写重排序到构造函数之外。

JMM编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外

读final域重排序规则

一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作

分析上面代码示例 reader()方法包含3个操作。

初次读引用变量obj。

初次读引用变量obj指向对象的普通域j。

初次读引用变量obj指向对象的final域

final域为引用类型

</>复制代码

  1. /**
  2. *假设首先线程A执行writeOne方法,执行完后线程B执行writetwo()方法,执行完后线程执行reader()方法
  3. *1是对final域的写入,2是对这个final域引用的对象的成员域的写入,3是把被构造的对象的引用赋值给某个引用变量。这里除了前面提到的1不能和3重排序外,23也不能重排序
  4. */
  5. public class FinalReferenceExample {
  6. final int[] intArray;
  7. static FinalReferenceExample obj;
  8. public FinalReferenceExample () {
  9. intArray = new int[1]; //1
  10. intArray[0] = 1; //2
  11. }
  12. public static void writerOne () { //线程A
  13. obj = new FinalReferenceExample (); //3
  14. }
  15. public static void writerTwo () { //线程B
  16. obj.intArray[0] = 2; //4
  17. }
  18. public static void reader () { //线程C
  19. if (obj != null) { //5
  20. int temp1 = obj.intArray[0]; //6
  21. }
  22. }

本例final域为一个引用类型,它引用一个int型的数组对象。对于引用类型,写final域的重排序规则对编译器和处理器增加了如下约束:在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

final引用不能从构造函数内溢出

</>复制代码

  1. /**
  2. * 步骤2使得构造函数还未完成就对reader线程可见
  3. **/
  4. public class FinalReferenceEscapeExample {
  5. final int i;
  6. static FinalReferenceEscapeExample obj;
  7. public FinalReferenceEscapeExample () {
  8. i = 1; // 1写final域
  9. obj = this; // 2 this引用在此"逸出"
  10. }
  11. public static void writer() {
  12. new FinalReferenceEscapeExample ();
  13. }
  14. public static void reader() {
  15. if (obj != null) { // 3
  16. int temp = obj.i; // 4
  17. }
  18. }

结论:在构造函数返回前,被构造对象的引用不能为其他线程所见

双重检查锁定与延迟初始化 java内存模型综述

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68857.html

相关文章

  • 来,了解一下Java内存模型(JMM)

    摘要:因为管理人员是了解手下的人员以及自己负责的事情的。处理器优化和指令重排上面提到在在和主存之间增加缓存,在多线程场景下会存在缓存一致性问题。有没有发现,缓存一致性问题其实就是可见性问题。 网上有很多关于Java内存模型的文章,在《深入理解Java虚拟机》和《Java并发编程的艺术》等书中也都有关于这个知识点的介绍。但是,很多人读完之后还是搞不清楚,甚至有的人说自己更懵了。本文,就来整体的...

    kviccn 评论0 收藏0
  • 来,了解一下Java内存模型(JMM)

    摘要:因为管理人员是了解手下的人员以及自己负责的事情的。处理器优化和指令重排上面提到在在和主存之间增加缓存,在多线程场景下会存在缓存一致性问题。有没有发现,缓存一致性问题其实就是可见性问题。 网上有很多关于Java内存模型的文章,在《深入理解Java虚拟机》和《Java并发编程的艺术》等书中也都有关于这个知识点的介绍。但是,很多人读完之后还是搞不清楚,甚至有的人说自己更懵了。本文,就来整体的...

    eccozhou 评论0 收藏0
  • 深入理解Java内存模型(七)——总结

    摘要:编译器,和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。正确同步的多线程程序的执行将具有顺序一致性程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。 前情提要 深入理解Java内存模型(六)——final 处理器内存模型 顺序一致性内存模型是一个理论参考模型,JMM和处理器内存模型在设计时通常会把顺序一致性内存模型作为参照。JMM和处理器内...

    paney129 评论0 收藏0
  • 简述Java内存模型

    摘要:内存模型即,简称,其规范了虚拟机与计算机内存时如何协同工作的,规定了一个线程如何和何时看到其他线程修改过的值,以及在必须时,如何同步访问共享变量。内存模型要求调用栈和本地变量存放在线程栈上,对象存放在堆上。 Java内存模型即Java Memory Model,简称JMM,其规范了Java虚拟机与计算机内存时如何协同工作的,规定了一个线程如何和何时看到其他线程修改过的值,以及在必须时,...

    ACb0y 评论0 收藏0
  • JVM 探究(一):JVM内存模型概念模型

    摘要:作为一个程序员,不了解内存模型就不能写出能够充分利用内存的代码。程序计数器是在电脑处理器中的一个寄存器,用来指示电脑下一步要运行的指令序列。在虚拟机中,本地方法栈和虚拟机栈是共用同一块内存的,不做具体区分。 作为一个 Java 程序员,不了解 Java 内存模型就不能写出能够充分利用内存的代码。本文通过对 Java 内存模型的介绍,让读者能够了解 Java 的内存的分配情况,适合 Ja...

    cnTomato 评论0 收藏0

发表评论

0条评论

2i18ns

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<