并发编程(三):内存模型基础

1.内存模型基础

Java内存模型,Java Memory Model简称JMM

1.1 两个关键问题

线程间如何通信和线程间如何同步

a.如何通信

两种机制:共享内存,消息通信

  • 共享内存

    线程共享程序公共状态,通过读写公共状态进行通信

  • 消息通信

    线程间通过发送消息显示通信

Java采用的是共享内存模型,线程间是隐式通信,由JMM控制

b.如何同步

  • 共享内存模型:

    隐式通信,显式同步,程序员指定线程之间的互斥

  • 消息通信模型:

    显式通信,隐式同步,消息发送需在消息接收之前

1.2 JMM抽象结构

共享变量

  • 实例域,静态域和数组元素存储在堆内存中,线程共享(共享变量)
  • 局部变量,方法定义参数和异常处理器参数线程间不共享,不受JMM影响

内存抽象模型

本地内存是JMM的一个抽象概念,并不是真实存在的。

线程通信模型

线程1给线程2发送消息示意图:

1.3 重排序简介

重排序流程图:

重排序会导致可见性问题,JMM可以禁止特定类型编译器重排序,或者通过插入内存屏障来阻止特定类型的处理器重排序来解决可见性问题。

JMM属于语言级的内存模型,可以确保为程序员提供一致的内存可见性保证

1.4 处理器重排序规则

处理器对内存读写操作的执行顺序,不一定与内存实际发生的读/写操作一致

处理器重排序规则:

处理器↓ | 规则→ 读 -读 读-写(Load-Store) 写-写 写-读 数据依赖
SPARC-TSO N N(不允许重排序) N Y N
X86 N N N Y N
IA64 Y Y(允许重排序) Y Y N
PowerPc Y Y Y Y N

写缓冲区可能会导致写读重排序,这些处理器都有写缓冲区。

为保证内存可见性,编译器会在生成的指令序列特定位置插入内存屏障来阻止特定类型的处理器指令级重排序(第一次重排和第二次重排之间)。

内存屏障分类:

屏障类型 说明
读-读屏障(LoadLoad Barriers) 阻止读-读重排序,Load1 LoadLoad Load2,保证Load1先与Load2执行
写-写屏障(StoreStore Barriers) 阻止写-写重排序,Store1 StoreStore Store2,确保Store1先于Store2更新主存
读-写屏障(LoadStore Barriers) 阻止读-写重排序,确保屏障前的读指令先于屏障后的写指令执行
写-读屏障(StoreLoad Barriers) 同时具有其他三个屏障效果, 使屏障前所有指令执行后才执行屏障后的指令,一般处理器都支持

1.5 happens-before简介

一个操作的执行结果要对另一个操作可见,那么这两个操作必须要存在happens-before关系(可见性)

与程序员相关的happens-before(先行性)规则如下:(共有8个)

规则 内容
程序顺序规则 一个线程中的每个操作,先于该线程中任意后续操作
监视器锁规则 对一个锁的解锁,先于后续对该锁的加锁
Volatile变量规则 对一个Volitile变量的写,先于后续对该变量的读
传递性 A先于B,B先于C,那么A先于C

happens-before与JMM的关系图:

2.重排序

重排序是编译器和处理器为了优化性能对指令序列进行重新排序的一种手段

2.1 数据依赖性

如果两个操作访问同一个变量,且这两个操作有一个为写操作,那么这两个操作之间就存在数据依赖性。(仅针对单核单线程内)

分为三种类型:读后写,写后读,写后写

编译器和处理器不会改变存在数据依赖性的两个操作的执行顺序

不同处理器和不同线程之间的数据依赖性不被编译器和处理器考虑

2.2 as-if-serial语义

不管怎么重排序,(单线程)程序的执行结果不能改变

JMM,编译器,runtime(运行时),处理器都必须严格遵守as-if-serial语义

计算机软硬件技术有一个共同目标:在不改变程序执行结果的前提下,尽可能提高并行度

2.3 重排序和多线程

公共类代码如下:

Class Example{
    int a=0;
    boolean flag=false;
    
    public void write(){
       a=1;				//步骤1
       flag=true;		//步骤2
    }
    
    public void read(){
        if(flag){		 //步骤3
            int i=a*a;	 //步骤4
        }
    }
}

步骤1,2和步骤3,4之间都没有数据依赖性,1和2,3和4可以重排序。

步骤12重排序:

步骤34重排序:

两种重排序都会导致执行结果异常(i=0),重排序破坏了多线程的语义

  • 单线程程序中,对存在控制依赖(不是数据依赖)的操作重排序不会改变执行结果
  • 多线程中,对存在控制以来的操作重排序可能会程序改变执行结果

3. 顺序一致性

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

3.1 数据竞争

定义:

在一个线程中写一个变量
在另一个线程中读同一个变量
读写没有通过同步来排序

如果程序是正确同步的,程序执行将具有顺序一致性,程序执行结果与该程序在顺序一致性内存模型中的执行结果相同。

常用同步是原语:sychronized,volatile和final

3.2 顺序一致性内存模型

模型图

任意时间点最多只能有一个线程可以连接到内存,每个操作都必须原子执行并且对所有线程可见

顺序一致性模型两大特性:

  • 一个线程中所有操作必须按照程序的顺序来执行
  • 所有线程都只能看到单一的执行顺序

执行效果

线程A含顺序执行操作:A1→A2→A3

线程B含顺序执行操作:B1→B2→B3

  • 在顺序一致性中执行效果1:

    A1→A2→A3→B1→B2→B3

  • 在顺序一致性中执行效果2:

    B1→A1→A2→B2→A3→B3

  • 在顺序一致性中执行效果3:

    A1→B1→A2→B2→A3→B3

……(还有好几种)……

整体执行可能是无序的,但是两个线程能看到相同的执行顺序,线程内仍然是顺序执行

JMM不保证顺序一致性

JMM写操作修改的是本地内存,未刷新到主存前仅对当前线程可见

JMM的基本方针:不改变(同步)程序的执行结果前提下,尽可能为编译器处理器优化提供方便(重排序)

3.3 未同步程序的执行特性

JMM为未同步和未正确同步的多线程程序提供最小安全性保证:线程读到的值要么是之前某个线程写入的值,要么是默认值

JMM与顺序一致性的差异

未同步程序在Java内存模型和顺序一致性模型执行的差异:

  1. 顺序一致性保证单线程内程序按顺序执行

    JMM不保证单线程内按顺序执行(重排序)

  2. 顺序一致性所有线程只能看见一致的操作执行顺序,JMM不保证

  3. 顺序一致性模型保证所有的读写操作都具有原子性

    JMM不保证对64位的long型和double型读写操作具有原子性

JMM64位操作

  • 数据通过总线在处理器和内存间传递,总线数据传递称为总线事务,包括读事务(内存→处理器)和写事务(处理器→内存),多个处理器同时向总线发起总线事务会导致总线仲裁,会挑选一个处理器执行总线事务,其他处理器等待。

  • 在一些32位的处理器上,要求对64位数据操作具有原子性会有较大开销,JVM可能会把一个64位写操作分为两个32位写操作来执行,这时就无法保证写操作的原子性了。

  • JSR-133内存模型(jdk1.5)之前,一个64位long/double的读/写操作都可以被分为两个32位操作。jdk1.5及以后则可以保证64位数据读的原子性。

posted @ 2021-03-11 20:25  菜鸟kenshine  阅读(86)  评论(0编辑  收藏  举报