加入收藏 | 设为首页 | 会员中心 | 我要投稿 威海站长网 (https://www.0631zz.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 运营中心 > 交互 > 正文

Java内存模型的深入理解

发布时间:2021-01-01 09:45:56 所属栏目:交互 来源:网络整理
导读:h3 id="基础"基础 h4 id="并发编程的模型分类"并发编程的模型分类 在并发编程需要处理的两个关键问题是:线程之间如何通信?和?线程之间如何同步。 通信?是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存?和?消息传递

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

总线的工作机制:

2018-02-27_22-53-53

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

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

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

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

2018-02-27_23-06-59

如上图所示,假设处理器 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中都必须具有原子性(即任意读操作必须要在单个读事务中执行)。

举个例子:

public void set(long l) { a = l; //单个 volatile 变量的写 } public long get() { return a; //单个 volatile 变量的读 } public void getAndIncreament() { a++; // 复合(多个) volatile 变量的读 /写 }

}  

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

public synchronized void set(long l) { //对单个普通变量的写用同一个锁同步 a = l; } public synchronized long get() { //对单个普通变量的读用同一个锁同步 return a; } public void getAndIncreament() { //普通方法调用 long temp = get(); //调用已同步的读方法 temp += 1L; //普通写操作 set(temp); //调用已同步的写方法 }

}

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

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

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

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

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

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

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

(编辑:威海站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

热点阅读