《深入理解 Java 内存模型》读书笔记

【mip-NeRF】使用一组二维图像渲染3D场景视频(mip-NeRF论文复现) 本文对mip-NeRF模型的pytorch实现进行了复现 阅读详情

img

前提

《深入理解 Java 内存模型》程晓明著,该书在以前看过一遍,现在学的东西越多,感觉那块越重要,于是又再细看一遍,于是便有了下面的读书笔记总结。全书页数虽不多,内容讲得挺深的。细看的话,也是挺花时间的,看完收获绝对挺大的。也建议 Java 开发者都去看看。里面主要有 Java 内存模型的基础、重排序、顺序一致性、Volatile 关键字、锁、final。本文参考书中内容。

基础

并发编程的模型分类

在并发编程需要处理的两个关键问题是:线程之间如何通信 和 线程之间如何同步。

通信

通信 是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存 和 消息传递。

在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。

在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。

同步

同步 是指程序用于控制不同线程之间操作发生相对顺序的机制。

在共享内存的并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。

在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

Java 的并发采用的是共享内存模型,Java 线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

Java 内存模型的抽象

在 Java 中,所有实例域、静态域 和 数组元素存储在堆内存中,堆内存在线程之间共享。局部变量、方法定义参数 和 异常处理器参数 不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

Java 线程之间的通信由 Java 内存模型(JMM)控制。JMM 决定了一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM 定义了线程与主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每一个线程都有一个自己私有的本地内存,本地内存中存储了该变量以读/写共享变量的副本。本地内存是 JMM 的一个抽象概念,并不真实存在。

JMM 抽象示意图:

img

从上图来看,如果线程 A 和线程 B 要通信的话,要如下两个步骤:

1、线程 A 需要将本地内存 A 中的共享变量副本刷新到主内存去

2、线程 B 去主内存读取线程 A 之前已更新过的共享变量

步骤示意图:

img

举个例子:

本地内存 A 和 B 有主内存共享变量 X 的副本。假设一开始时,这三个内存中 X 的值都是 0。线程 A 正执行时,把更新后的 X 值(假设为 1)临时存放在自己的本地内存 A 中。当线程 A 和 B 需要通信时,线程 A 首先会把自己本地内存 A 中修改后的 X 值刷新到主内存去,此时主内存中的 X 值变为了 1。随后,线程 B 到主内存中读取线程 A 更新后的共享变量 X 的值,此时线程 B 的本地内存的 X 值也变成了 1。

整体来看,这两个步骤实质上是线程 A 再向线程 B 发送消息,而这个通信过程必须经过主内存。JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 Java 程序员提供内存可见性保证。

重排序

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三类:

1、编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

2、指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

3、内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从 Java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

img

上面的这些重排序都可能导致多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求 Java 编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。

JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

处理器重排序

现代的处理器使用写缓冲区来临时保存向内存写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,可以减少对内存总线的占用。虽然写缓冲区有这么多好处,但每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致!

举个例子:

img

假设处理器A和处理器B按程序的顺序并行执行内存访问,最终却可能得到 x = y = 0。具体的原因如下图所示:

exam1-ansexam1-ans

处理器 A 和 B 同时把共享变量写入在写缓冲区中(A1、B1),然后再从内存中读取另一个共享变量(A2、B2),最后才把自己写缓冲区中保存的脏数据刷新到内存中(A3、B3)。当以这种时序执行时,程序就可以得到 x = y = 0 的结果。

从内存操作实际发生的顺序来看,直到处理器 A 执行 A3 来刷新自己的写缓存区,写操作 A1 才算真正执行了。虽然处理器 A 执行内存操作的顺序为:A1 -> A2,但内存操作实际发生的顺序却是:A2 -> A1。此时,处理器 A 的内存操作顺序被重排序了。

这里的关键是,由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操作重排序。

内存屏障指令

为了保证内存可见性,Java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM 把内存屏障指令分为下列四类:

img

happens-before

JSR-133 内存模型使用 happens-before 的概念来阐述操作之间的内存可见性。在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

与程序员密切相关的 happens-before 规则如下:

  • 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个监视器的解锁,happens-before 于随后对这个监视器的加锁。
  • volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
  • 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。

注意,两个操作之间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before 仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。

happens-before 与 JMM 的关系如下图所示:

img

如上图所示,一个 happens-before 规则对应于一个或多个编译器和处理器重排序规则。

数据依赖性

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分下列三种类型:

名称
代码示例
说明
写后读a = 1; b = a;写一个变量之后,再读这个位置。
写后写a = 1; a = 2;写一个变量之后,再写这个变量。
读后写a = b; b = 1;读一个变量之后,再写这个变量。

上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。

前面提到过,编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

注意,这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。

as-if-serial 语义

as-if-serial 语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守 as-if-serial 语义。

为了遵守 as-if-serial 编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是如果操作之间没有数据依赖关系,这些操作就可能被编译器和处理器重排序。

举个例子:

1double pi = 3.14;     //A
2double r  = 1.0;       //B
3double area = pi * r * r;     //C

上面三个操作的数据依赖关系如下图所示:

img

如上图所示,A 和 C 之间存在数据依赖关系,同时 B 和 C 之间也存在数据依赖关系。因此在最终执行的指令序列中,C 不能被重排序到 A 和 B 的前面(C 排到 A 和 B 的前面,程序的结果将会被改变)。但 A 和 B 之间没有数据依赖关系,编译器和处理器可以重排序 A 和 B 之间的执行顺序。下图是该程序的两种执行顺序:

img

在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽可能的开发并行度。编译器和处理器遵从这一目标,从 happens-before 的定义我们可以看出,JMM 同样遵从这一目标。

重排序对多线程的影响

举例:

 1class Demo {
 2  int a = 0;
 3  boolean flag = false;
 4
 5  public void write() {
 6    a = 1;            //1
 7    flag = true;    //2
 8  }
 9
10  public void read() {
11    if(flag) {            //3
12      int i = a * a;    //4
13    }
14  }
15}

由于操作 1 和 2 没有数据依赖关系,编译器和处理器可以对这两个操作重排序;操作 3 和操作 4 没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。

1、当操作 1 和操作 2 重排序时,可能会产生什么效果?

img

如上图所示,操作 1 和操作 2 做了重排序。程序执行时,线程 A 首先写标记变量 flag,随后线程 B 读这个变量。由于条件判断为真,线程 B 将读取变量 a。此时,变量 a 还根本没有被线程 A 写入,在这里多线程程序的语义被重排序破坏了!

2、当操作 3 和操作 4 重排序时会产生什么效果(借助这个重排序,可以顺便说明控制依赖性)。

img

在程序中,操作 3 和操作 4 存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测(Speculation)执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程 B 的处理器可以提前读取并计算 a * a,然后把计算结果临时保存到一个名为重排序缓冲(reorder buffer ROB)的硬件缓存中。当接下来操作 3 的条件判断为真时,就把该计算结果写入变量 i 中。

从图中我们可以看出,猜测执行实质上对操作3和4做了重排序。重排序在这里破坏了多线程程序的语义!

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是 as-if-serial 语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

顺序一致性

顺序一致性内存模型

顺序一致性内存模型有两大特性:

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

顺序一致性内存模型为程序员提供的视图如下:

img

在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程,同时每一个线程必须按照程序的顺序来执行内存读/写操作。从上面的示意图我们可以看出,在任意时间点最多只能有一个线程可以连接到内存。当多个线程并发执行时,图中的开关装置能把所有线程的所有内存读/写操作串行化。

举个例子:

假设有两个线程 A 和 B 并发执行。其中 A 线程有三个操作,它们在程序中的顺序是:A1 -> A2 -> A3。B 线程也有三个操作,它们在程序中的顺序是:B1 -> B2 -> B3。

假设这两个线程使用监视器锁来正确同步:A 线程的三个操作执行后释放监视器锁,随后 B 线程获取同一个监视器锁。那么程序在顺序一致性模型中的执行效果将如下图所示:

img

现在我们再假设这两个线程没有做同步,下面是这个未同步程序在顺序一致性模型中的执行示意图:

img

未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只能看到一个一致的整体执行顺序。以上图为例,线程 A 和 B 看到的执行顺序都是:B1 -> A1 -> A2 -> B2 -> A3 -> B3。之所以能得到这个保证是因为顺序一致性内存模型中的每个操作必须立即对任意线程可见。

但是,在 JMM 中就没有这个保证。未同步程序在 JMM 中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致。比如,在当前线程把写过的数据缓存在本地内存中,在还没有刷新到主内存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,会认为这个写操作根本还没有被当前线程执行。只有当前线程把本地内存中写过的数据刷新到主内存之后,这个写操作才能对其他线程可见。在这种情况下,当前线程和其它线程看到的操作执行顺序将不一致。

同步程序的顺序一致性效果

下面我们对前面的示例程序用锁来同步,看看正确同步的程序如何具有顺序一致性。

请看下面的示例代码:

 1class demo {
 2  int a = 0;
 3  boolean flag = false;
 4
 5  public synchronized void write() {    //获取锁
 6    a = 1;
 7    flag = true;
 8  }                                        //释放锁
 9
10  public synchronized void read() {        //获取锁
11    if(flag) {
12      int i = a;
13    }
14  }                                        //释放锁
15}

上面示例代码中,假设 A 线程执行 write() 方法后,B 线程执行 reade() 方法。这是一个正确同步的多线程程序。根据JMM规范,该程序的执行结果将与该程序在顺序一致性模型中的执行结果相同。下面是该程序在两个内存模型中的执行时序对比图:

img

在顺序一致性模型中,所有操作完全按程序的顺序执行。而在 JMM 中,临界区内的代码可以重排序(但 JMM 不允许临界区内的代码“逸出”到临界区之外,那样会破坏监视器的语义)。JMM 会在退出临界区和进入临界区这两个关键时间点做一些特别处理,使得线程在这两个时间点具有与顺序一致性模型相同的内存视图。虽然线程 A 在临界区内做了重排序,但由于监视器的互斥执行的特性,这里的线程 B 根本无法“观察”到线程 A 在临界区内的重排序。这种重排序既提高了执行效率,又没有改变程序的执行结果。

从这里我们可以看到 JMM 在具体实现上的基本方针:在不改变(正确同步的)程序执行结果的前提下,尽可能的为编译器和处理器的优化打开方便之门。

未同步程序的执行特性

未同步程序在 JMM 中的执行时,整体上是无序的,其执行结果无法预知。未同步程序在两个模型中的执行特性有下面几个差异:

  1. 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而 JMM 不保证单线程内的操作会按程序的顺序执行(比如上面正确同步的多线程程序在临界区内的重排序)。
  2. 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而 JMM 不保证所有线程能看到一致的操作执行顺序。
  3. JMM 不保证对 64 位的 long 型和 double 型变量的读/写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子 。

第三个差异与处理器总线的工作机制密切相关。在计算机中,数据通过总线在处理器和内存之间传递。每次处理器和内存之间的数据传递都是通过总线事务来完成的。总线事务包括读事务和写事务。读事务从内存传送数据到处理器,写事务从处理器传递数据到内存,每个事务会读/写内存中一个或多个物理上连续的字。总线会同步试图并发使用总线的事务。在一个处理器执行总线事务期间,总线会禁止其它所有的处理器和 I/O 设备执行内存的读/写。

总线的工作机制:

img

如上图所示,假设处理器 A、B、和 C 同时向总线发起总线事务,这时总线仲裁会对竞争作出裁决,假设总线在仲裁后判定处理器 A 在竞争中获胜(总线仲裁会确保所有处理器都能公平的访问内存)。此时处理器 A 继续它的总线事务,而其它两个处理器则要等待处理器 A 的总线事务完成后才能开始再次执行内存访问。假设在处理器 A 执行总线事务期间(不管这个总线事务是读事务还是写事务),处理器 D 向总线发起了总线事务,此时处理器 D 的这个请求会被总线禁止。

总线的这些工作机制可以把所有处理器对内存的访问以串行化的方式来执行;在任意时间点,最多只能有一个处理器能访问内存。这个特性确保了单个总线事务之中的内存读/写操作具有原子性。

在一些 32 位的处理器上,如果要求对 64 位数据的写操作具有原子性,会有比较大的开销。为了照顾这种处理器,Java 语言规范鼓励但不强求 JVM 对 64 位的 long 型变量和 double 型变量的写具有原子性。当 JVM 在这种处理器上运行时,会把一个 64 位 long/ double 型变量的写操作拆分为两个 32 位的写操作来执行。这两个 32 位的写操作可能会被分配到不同的总线事务中执行,此时对这个 64 位变量的写将不具有原子性。

当单个内存操作不具有原子性,将可能会产生意想不到后果。请看下面示意图:

img

如上图所示,假设处理器 A 写一个 long 型变量,同时处理器 B 要读这个 long 型变量。处理器 A 中 64 位的写操作被拆分为两个 32 位的写操作,且这两个 32 位的写操作被分配到不同的写事务中执行。同时处理器 B 中 64 位的读操作被分配到单个的读事务中执行。当处理器 A 和 B 按上图的时序来执行时,处理器 B 将看到仅仅被处理器 A “写了一半“的无效值。

注意,在 JSR -133 之前的旧内存模型中,一个 64 位 long/ double 型变量的读/写操作可以被拆分为两个 32 位的读/写操作来执行。从 JSR -133 内存模型开始(即从JDK5开始),仅仅只允许把一个 64 位 long/ double 型变量的写操作拆分为两个 32 位的写操作来执行,任意的读操作在JSR -133中都必须具有原子性(即任意读操作必须要在单个读事务中执行)。

Volatile

Volatile 特性

举个例子:

 1public class VolatileTest {
 2    volatile long a = 1L;         // 使用 volatile 声明 64 位的 long 型
 3
 4    public void set(long l) {
 5        a = l;                  //单个 volatile 变量的写
 6    }
 7
 8    public long get() {
 9        return a;               //单个 volatile 变量的读
10    }
11
12    public void getAndIncreament() {
13        a  ;                    // 复合(多个) volatile 变量的读 /写
14    }
15}

假设有多个线程分别调用上面程序的三个方法,这个程序在语义上和下面程序等价:

 1public class VolatileTest {
 2    long a = 1L;                 // 64 位的 long 型普通变量
 3
 4    public synchronized void set(long l) {    //对单个普通变量的写用同一个锁同步
 5        a = l;                
 6    }
 7
 8    public synchronized long get() {        //对单个普通变量的读用同一个锁同步
 9        return a;          
10    }
11
12    public void getAndIncreament() {        //普通方法调用
13          long temp = get();                  //调用已同步的读方法
14        temp  = 1L;                            //普通写操作                        
15          set(temp);                          //调用已同步的写方法
16    }
17}

如上面示例程序所示,对一个 volatile 变量的单个读/写操作,与对一个普通变量的读/写操作使用同一个锁来同步,它们之间的执行效果相同。

锁的 happens-before 规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入。

锁的语义决定了临界区代码的执行具有原子性。这意味着即使是 64 位的 long 型和 double 型变量,只要它是 volatile变量,对该变量的读写就将具有原子性。如果是多个 volatile 操作或类似于 volatile 这种复合操作,这些操作整体上不具有原子性。

简而言之,volatile 变量自身具有下列特性:

  • 可见性。对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入。
  • 原子性:对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile 这种复合操作不具有原子性。
volatile 写-读的内存定义

  • 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。
  • 当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

假设上面的程序 flag 变量用 volatile 修饰

img

volatile 内存语义的实现

下面是 JMM 针对编译器制定的 volatile 重排序规则表:

img

为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

下面是基于保守策略的 JMM 内存屏障插入策略:

  • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
  • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

下面是保守策略下,volatile 写操作 插入内存屏障后生成的指令序列示意图:

img

下面是在保守策略下,volatile 读操作 插入内存屏障后生成的指令序列示意图:

img

上述 volatile 写操作和 volatile 读操作的内存屏障插入策略非常保守。在实际执行时,只要不改变 volatile 写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。

锁

锁释放和获取的内存语义

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

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

锁内存语义的实现

借助 ReentrantLock 来讲解,PS:后面专门讲下这块(ReentrantLock、Synchronized、公平锁、非公平锁、AQS等),可以看看大明哥的博客:http://cmsblogs.com/?p=2210

concurrent 包的实现

如果我们仔细分析 concurrent 包的源代码实现,会发现一个通用化的实现模式:

  1. 首先,声明共享变量为 volatile;
  2. 然后,使用 CAS 的原子条件更新来实现线程之间的同步;
  3. 同时,配合以 volatile 的读/写和 CAS 所具有的 volatile 读和写的内存语义来实现线程之间的通信。

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic 包中的类),这些 concurrent 包中的基础类都是使用这种模式来实现的,而 concurrent 包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent 包的实现示意图如下:

img

final

对于 final 域,编译器和处理器要遵守两个重排序规则:

  1. 在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  2. 初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序。
写 final 域的重排序规则

写 final 域的重排序规则禁止把 final 域的写重排序到构造函数之外。这个规则的实现包含下面2个方面:

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

在一个线程中,初次读对象引用与初次读该对象包含的 final 域,JMM 禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读 final 域操作的前面插入一个 LoadLoad 屏障。

final 域是引用类型

对于引用类型,写 final 域的重排序规则对编译器和处理器增加了如下约束:

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

总结

JMM,处理器内存模型与顺序一致性内存模型之间的关系

JMM 是一个语言级的内存模型,处理器内存模型是硬件级的内存模型,顺序一致性内存模型是一个理论参考模型。下面是语言内存模型,处理器内存模型和顺序一致性内存模型的强弱对比示意图:

img

JMM 的设计示意图

img

JMM 的内存可见性保证

Java 程序的内存可见性保证按程序类型可以分为下列三类:

1.单线程程序。单线程程序不会出现内存可见性问题。编译器,runtime 和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。

2.正确同步的多线程程序。正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。这是 JMM 关注的重点,JMM通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。

3.未同步/未正确同步的多线程程序。JMM 为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false)。

下图展示了这三类程序在 JMM 中与在顺序一致性内存模型中的执行结果的异同:

img

推荐阅读:

img

终端以太网接入Internet实验 实验过程: 1.布局 2.连接 3.配置Route0和Route1 4.配置RIP 5.static: 6.查看Route0,Route1的路由表: 7.完成PC2和server0 IP地址,子网掩码和默认网关地址的配置: 8.命令行: 9.启动PC0桌面下的PPPoe宽带连接程序: 10.启动PC1桌面下的PPPoe宽带连接程序: 11.验证PC0和PC2的ICMP报文传输过程 12.验证PC1和server0的ICMP报文传输过程 关键命令说明 阅读详情

相关推荐

内网(极空间)搭建gitlab跳板机转发端口及域名配置

然后,回头尝试用内网地址+15022 clone,成功,用公网的ip+15222 clone失败,那就是定位到时nginx转发的问题了。首先想到的就是 git clone是通过ssh的22端口进行下载的,那就需要打通22端口,则又配置22端口的nginx转发。然后,我通过ssh命令,做一个端口转发,以实现内网穿透,将服务端口链接到跳板机上。然后穿透到阿里云服务器跳板机的端口,只能内网访问,需要用nginx做个转发,才能用公网ip进行访问。然后,如上,我的80端口,映射15080,22端口映射15022。

God丶Davide的博客 6076

WPF窗体加载的事件顺序

对一些相对复杂的业务需求而言,弄懂window各事件的执行顺序会让我们游刃有余。

IQQQQU的博客 6362

ArcGIS 10.8软件安装包免费下载及安装教程

ArcGIS 10.8安装教程

qq_44894692的博客 2万+

值得一看的好书

读不在三更五鼓,功只怕一曝十寒!岁月从不辜负奋斗的人,愿努力拼搏学习的你,都能收获美好的前程! 承遇朝霞,年少正恰。 整装戎马,刻印风华。 目录 一、推荐-日常推荐优秀书籍 二、Java/Spring-16本提升基本功的书籍 三、DB-2本数据库优秀书籍 四、Redis-深入学习Redis 五、架构&设计-提升架构能力和编写优秀代码 六、号主原创书籍[免费下载]-收集优秀原创技术号主PDF书籍 七、其他系列-拓展学习技能增强个人能力 八、人性-...

Mick_^_^的博客 1456

冰河木马实验(V8.4)

简述: 百科词条 冰河木马开发于1999年,跟灰鸽子类似,在设计之初,开发者的本意是编写一个功能强大的远程控制软件。但一经推出,就依靠其强大的功能成为了黑客们发动入侵的工具,并结束了国外木马一统天下的局面,跟后来的灰鸽子等等成为国产木马的标志和代名词。 冰河的服务器端程序为G-server.exe,客户端程序为G-client.exe,默认连接端口为7626。其中攻击者使用G-client.e...

野原新之助 1万+

Python爬虫实战:5分钟搞定东方财富网股票数据抓取(附完整代码)

本文详细介绍了如何使用Python爬虫快速抓取东方财富网的股票数据,包括环境配置、API分析、核心代码实现及优化技巧。通过简单的requests和pandas库,5分钟内即可获取高质量股票数据,适合量化交易和数据分析初学者。

weixin_29040367的博客 1187

【Java】进程通信(共享内存)

`Java IO `操作的`BufferedReader` 、 `BufferedInputStream` 等相信大家都很熟悉,不过在 `Java NIO `中引入了一种基于` MappedByteBuffer `操作大文件的方式,其读写性能极高。 `MappedByteBuffer` 为共享内存缓冲区,实际上是一个磁盘文件的内存映射,实现内存与文件的同步变化,可有效地保证共享内存的实现。

泠青沼 3172

Cartographer从安装到实战:手把手教你运行2D/3D建图与定位

本文提供了一份详尽的Cartographer从零安装到实战应用的完整指南。内容涵盖环境准备、依赖编译、源码构建等关键步骤,并手把手指导读者运行2D/3D官方数据集进行建图与定位测试。文章深入解析了参数配置与自定义数据适配方法,帮助开发者快速掌握这一强大的实时同步定位与建图工具,并将其应用于机器人导航与自动驾驶等实际场景。

rice5的博客 952

Java的共享内存使用

java共享内存使用

meilidekcl的博客 2306

微信小程序 - 实现仿小红书轮播图组件功能,类似小红书笔记页面图片展示播放效果,微信小程序仿小红书幻灯片轮播支持视频播放、图片预览,自动适应图片或视频大小可左右滑动切换(小程序精美轮播图,丝滑切换)

微信小程序,轮播图组件,微信小程序仿小红书轮播图,小红书轮播图仿造,微信小程序轮播图,微信小程序幻灯片,微信小程序高仿小红书轮播图实现,小程序实现类似小红书一样的轮播图组件,轮播图插件,精致轮播图,精美丝滑流畅切换的轮播图,轮播图组件,小红薯轮播图,关于小红书的小程序不同数量的轮播图,展示不一样的轮播指示点的实现,微信小程序仿小红书图片轮播,开发微信小程序模仿小红书,高仿小红书Banner的指示器,小红书小程序笔记详情轮播图指示点的随图片数量的变化动态变化,微信小程序swiper轮播图,js轮播图,html

✅ 精选推荐文章 6934

DeerFlow 系列教程 第三篇 | 核心技术深度剖析:Skills、Memory 与 Context Engineering

摘要化是 Context Engineering 中最关键的组件——它直接决定了长对话的可用性。Skills 技能系统通过渐进式加载的三层模型,在保持系统提示词轻量的同时,支持无限数量的技能扩展。从发现到执行的 10 个生命周期阶段,配合 AI 驱动的安全扫描和多层缓存,构成了一个生产级的技能管理框架。Memory 长期记忆系统通过文件系统持久化、LLM 驱动的信息提取和防抖动异步更新,实现了跨会话的认知持久化。六类事实分类体系和修正信号检测机制,让 Agent 能够从交互中持续学习和改进。

weixin_43674738的博客 280

java steam 去重_深入理解 Java 内存模型读书笔记

点击蓝色“SpringForAll社区”关注我哟前提《深入理解 Java 内存模型》程晓明著,该书在以前看过一遍,现在学的东西越多,感觉那块越重要,于是又再细看一遍,于是便有了下面的读书笔记总结。全书页数虽不多,内容讲得挺深的。细看的话,也是挺花时间的,看完收获绝对挺大的。也建议 Java 开发者都去看看。里面主要有 Java 内存模型的基础、重排序、顺序一致性、Volatile 关键字...

weixin_31653971的博客 131

【Codecs系列】x265编码器(十二):子像素运动搜索

在基于块运动补偿的视频编码框架中,运动搜索是最为重要的环节之一,同时也是编码端最为耗时的模块。运动搜索包括整像素运动搜索和子像素运动搜索,其中整像素运动搜索已经有很多快速算法,比如经典的三步搜索法、菱形搜索、六边形搜索以及TZSearch等等;但子像素搜索的快速算法却比较少,本文主要讲解x265编码器中的子像素搜索方法以及相关的快速搜索方法。

优快云明星博主,认证博客专家,视频、Matlab领域优质创作者。 749

转《深入理解 Java 内存模型》读书笔记

转:https://mp.weixin.qq.com/s/2hA6u4hLEPWlTPdD-XB-bg 前提 《深入理解 Java 内存模型》程晓明著,该书在以前看过一遍,现在学的东西越多,感觉那块越重要,于是又再细看一遍,于是便有了下面的读书笔记总结。全书页数虽不多,内容讲得挺深的。细看的话,也是挺花时间的,看完收获绝对挺大的。也建议 Java 开发者都去看看。里面主要有 Java 内存模型...

weixin_30648587的博客 81

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

java 内存模型书籍_《深入理解 Java 内存模型》读书笔记

前提《深入理解 Java 内存模型》程晓明著,该书在以前看过一遍,现在学的东西越多,感觉那块越重要,于是又再细看一遍,于是便有了下面的读书笔记总结。全书页数虽不多,内容讲得挺深的。细看的话,也是挺花时间的,看完收获绝对挺大的。也建议 Java 开发者都去看看。里面主要有 Java 内存模型的基础、重排序、顺序一致性、Volatile 关键字、锁、final。本文参考书中内容。关注我如果你想查看这本...

weixin_39572794的博客 237

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

Java内存模型——《深入理解java虚拟机》读书笔记

1、TPS是什么? Transactions Per Second,每秒事务处理数,代表着一秒内服务端平均能响应的请求总数。 2、java内存模型 内存模型可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的抽象过程。 java内存模型的主要目的是定义程序中各种变量的访问规则,即关注虚拟机把变量存储到内存和从内存中取出变量值这样的底层细节。 java内存模型规定了所有变量(不包括局部变量和参数)都存储在主内存中,每条线程还有自己的工作内存,该线程的工作内存中保存了被该线程使用的变量的主内存副本

azi_csdn的博客 228

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

java 内存模型书籍_深入理解java内存模型读书笔记

没怎么搞过java 不过看到这篇文章对自己有些理解有帮助 所以做了一个读书笔记JMM java memory modal JAVA内存模型重排序:前部分讲了几种层面的重排序会导致线程里面的顺序 并不会按照看到的顺序执行.重排序的几种分类:编译器优化,cpu优化(这个可以联想到现在的超线程技术,就是允许多个线程的指令在同一块物理单元里执行,一个物理cpu被当做两个逻辑cpu)还有就是内存系...

weixin_33479446的博客 203

【深入理解JAVA虚拟机】读书笔记——Java内存模型和线程

学习参考资料:周志明老师的著作《深入理解Java虚拟机(第3版)》 1.Java内存模型 需要注意Java内存模型指的是JMM,而不是运行时区域。 1.1主内存和工作内存 Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量存储到内存和从内存中取出变量值这样的底层细节。 内存模型如下图,线程运行时会将代码所需要的运行时数据从工作内存中获取,所以工作内存会从主内存中复制相关数据。那么也就是如果两个线程同时复制了主内存的共享变量并进行了修改,如果不加以控制,可能就会发生并发问题

遇事不决问清风的博客 406

深入理解java内存模型--读书笔记

深入理解java内存模型 java内存模型的抽象 java线程之间的通信由java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见 从抽象的角度来看,JMM决定了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memeory)中,每个线程都有一个私有的本地内存(local memeory),本地内存中存储了该线程以读/写共享变量的副本.本地内存是...

weixin_30394333的博客 101

《深入理解 Java 虚拟机》读书笔记:Java 内存模型与线程

由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统加入了一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存与处理器之间的缓冲:将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存中,这样处理器就无须等待缓慢的内存读写了。关于主内存与工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java 内存模型中定义了 8 种操作来完成,虚拟机必须保证每一种操作都是原子的、不可再分的。

qq_24428851的博客 820

《深入理解Java内存模型》 --- 读书笔记

JMM: Java Momory Model (内存模型)(JSR-133) 理解JMM有助于我们学习Concurrency。 看完了,有点难,了解一下吧,以后慢慢学习吧。   深入理解Java内存模型(一)——基础 内容概括:Java线程是有独立的,私有的数据空间,当然来源于main memory。   深入理解Java内存模型(二)——重排序 内容概括:CPU中,对...

179

《深入理解Java虚拟机》读书笔记---Java内存模型与线程

12.1概述 计算机CPU的运算速度远大于通信子系统的速度,计算机同时做多件事情

Peking之成长路程 318

深入理解java内存模型读书笔记

http://www.infoq.com/cn/minibooks/java_memory_model?utm_source=infoq&utm_medium=popular_links_homepage 原书地址 没怎么搞过java 不过看到这篇文章对自己有些理解有帮助 所以做了一个读书笔记 JMM java memory modal JAVA内存模型 重排序:前...

weixin_30390075的博客 82
下一篇: Spring Boot 面试的十个问题
SpringForAll
博客等级 码龄7年 15粉丝 79原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值