Java 内存模型

  |   0 评论   |   0 浏览

1.Java 内存模型的基础

1.1 并发编程模型的两个关键问题

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

在命令式编程中,线程通信机制有两种: 共享内存消息传递.

  • 共享内存并发模型:线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。
  • 消息传递并发模型:线程之间没有公共状态,线程之间必须通过发送消息显示进行通信。

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

**Java 采用的是共享内存模型。**线程之间的通信是隐式进行的。

1.2 Java 内存模型的抽象结构

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

Java 线程之间的通信由 Java 内存模型(简称 JMM)控制,JMM 决定一个线程对共享变量的写入何时对另一个线程可见

从抽象的角度来看,JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了用来读/写的共享变量的副本。本地内存是 JMM 的一个抽象概念,并不是真实存在的。

image.png

线程 A 和 线程 B 通信的话:

1)线程 A 把本地内存中更新过的共享变量刷新到主内存中去。

2)线程 B 到主内存中读取线程 A 之前已经更新过的共享变量。

image.png

JMM 通过 控制主内存与每个线程的本地内存之间的交互,来为 Java 程序员提供内存可见性保证

3.从源代码到指令序列的重排序

为了提高性能编译器处理器 会对指令做重排序。重排序分为 3 类:

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令集并行的重排序。现代处理器采用了 指令集并行技术 来将多条指令重叠执行。如果不存在数据依赖性,处理器可改变指令的执行顺序。
  • 内存系统的重排序。由于处理器使用缓存读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行。

image.png

重排序可能会导致多线程程序出现内存可见性问题。

4.并发编程模型的分类

现代的处理器使用 写缓存区 临时保存向内存写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。同时通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,减少对内存总线的占用。每个处理器上的写缓冲区,仅仅对它所在的处理器可见。

这个特性会对内存操作的执行顺序产生重要的影响:内存实际发生的读/写操作顺序可能与预期不同。

image.png

image.png

由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作顺序不一致。

image.png

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

image.png

StoreLoad Barriers 是一个"全能型"的屏障,它同时具体其他 3 个屏障的效果。

5. happens-before 简介

从 JDK5 开始,Java 使用新的 JSR - 133 内存模型。JSR-133 使用 happens-before 的概念来阐述操作之间的内存可见性。

在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在 happens-before 关系。

与程序员密切相关的 happends-before 规则如下。

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

happends-before 仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。

image.png

重排序

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

1.数据依赖性

如果两个操作访问同一个变量,且这两个操作之间有一个为写操作,此时这两个操作之间就存在数据依赖性

image.png

上面 3 中情况,只要重排序两个操作的执行顺序,程序的执行结果就会改变

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

2.as-if-serial 语义

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

为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖性关系的操作做重排序。

3.程序顺序规则

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

顺序一致性

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

1.数据竞争与顺序一致性

当程序未正确同步时,就可能会存在数据竞争。

在一个线程中写一个变量

在另一个线程读同一个变量

并且写和读没有通过同步来排序

JMM 对正确同步的多线程程序的内存一致性做了如下保证。

程序的执行将具有顺序一致性——即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。

  1. 顺序一致性内存模型

**顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。

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

image.png

volatile 的内存语义

1.volatile 的特性

理解 volatile 特性的一个好方法是把对 volatile 变量的单个读/写,看成是使用同一个锁对这些单个 读/写操作做了同步。

class VolatileFeaturesExample{
    volatile long vl = 0L;  //使用 volatile 声明 64 位的long 型变量
    public void set(long l){
        vl = l;   //单个 volatile 变量的写
    }
    public void getAndIncrement(){
         vl ++;   //复合(多个)volatile变量的读/写
    }
    public long get(){
          return vl;  //单个 volatile变量的读
    }
}
class VolatileFeaturesExample{
   long vl = 0L;    // 64位 的 long 型普通变量
   public synchronized void set(long l){ //对单个的普通变量的写用同一个锁同步
       vl = l;
   }
  
   public void getAndIncrement(){  //普通方法调用
         long temp = get();   //调用已经同步的读方法
         temp += 1L;   //普通写操作
         set(temp);      //调用已同步的写方法
   }

   public synchronized long get(){
        return vl;   //对单个的普通变量的读用同一个锁同步
   }
}

volatile 变量自身具有下列特性。

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

2.volatile 写-读建立的 happens-before 关系

从 JSR-133 开始(即从 JDK5 开始),volatile 变量的写-读可以实现线程之间的通信。

从内存语义的角度来说,volatile 的写-读与锁的释放-获取有相同的内存效果:volatile 写和锁的释放有相同的内存语义;volatile 读与锁的获取有相同的内存语义。

3.volatile 写-读的内存语义

volatile 写的内存语义如下。

当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。
image.png

volatile 读的内存语义如下。

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

image.png

4.volatile 内存语义的实现

JMM 如何实现 volatile 写/读的内存语义

为了实现 volatile 内存语义,JMM 会分别限制编译器重排序和处理器重排序。

image.png

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

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

image.png

不详细摘抄了,参考 《Java 并发编程》volatile 内存语义章节。

5. JSR-133 为什么要增强 volatile 的内存语义。

在旧的 Java 内存模型中允许 volatile 变量与普通变量重排序。

volatile 仅仅保证对单个 volatile 变量的读/写具有原子性。虽然在功能上 ,锁比 volatile 更强大,但在可伸缩性和执行性能上,volatile 更有优势。

锁的内存语义

锁可以让临界区互斥执行。

1.锁的 "释放-获取" 建立的 happends-before 关系

锁是 Java 并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。

下面是锁释放-获取示例代码。

class MonitorExample{
    int a = 0;
    public synchronized void writer(){
         a++;
    }
    public synchronized void reader(){
        int i = a;
    }
}

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

  • 1)根据程序次序规则,1happends-before2,2happends-before 3; 4 happends-before 5,5 happends-before 6
    1. 根据监视器规则, 3 happends-before 4.
    1. 根据 happends-before 的传递性, 2 happends-before 5。

2. 锁的释放和获取的内存语义

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

对比锁"释放-获取"的内存语义与 volatile 写-读的内存语义可以看出:锁释放与 volatile 写有相同的内存语义;锁获取与 voaltile 读有相同的内存语义。

锁获取和锁释放的内存语义:

  • 线程 A 释放一个锁,实质上是线程 A 向接下来将要获取这个锁的某个线程发出了(线程 A 对共享变量所做修改的)消息。
  • 线程 B 获取一个锁,实质上是线程 B 接受了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
  • 线程 A 释放锁,随后线程 B 获取这个锁,这个过程实质上是线程 A 通过主内存向线程 B 发送消息。

3.锁内存语义的实现

接下来将结束 ReentrantLock 的源代码,来分析锁内存语义的具体实现机制

ReetrantLock 的实现依赖于 Java 同步器框架 AbstractQueueSynchronizer,它使用一个整形的 voaltile 变量(命名为 state)来维护同步状态。 这个 volatile 变量是 ReentrantLock 内存语义实现的关键

ReerantLock 分为公平锁非公平锁

使用公平锁是,加锁方法 lock() 调用轨迹如下。

  1. ReetrantLock: lock()。
  2. FairSync:lock()。
  3. AbstractQueueSynchronizer: acquire(int arg)。
  4. ReetrantLock: tryAcquire(int acquires)。

在第 4 步真正开始加锁,下面是该方法的源代码

protected final boolean tryAcquire(int acquires){
   final Thread current = Thread.currentThread();
   int c= getState();   //获取锁的开始,首先读 volatile 变量 state
   if(c == 0){
        if(isFirst(current)  && compareAndSetState(0,acquires)){
             setExclusiveOwnerThread(current);
             return true;
        }
   } 
   else if(current == getExclusiveOwnerThread()){
       int nextc = c + acquires;
       if(nextc < 0){
           throw new Error("Maxinum lock count exceeded");
       }
       setState(nextc);
       return true;
   }
   return false;
}

从上面源代码中我们可以看出,加锁方法首先读 volatile 变量 state.

在使用公平锁时,解锁方法 unlock() 调用轨迹如下。

  1. ReentrantLock:unlock();
  2. AbstractQueueSynchronizer: release(int arg)。
  3. Sync: tryRelease(int releases).

在第 3 步真正开始释放锁,下面是该方法的源代码。

protected final boolean tryRelease(int releases){
    int c = getState() - releases;
    if(Thread.currentThread() != getExclusiveOwnerThread()){
        throw new IllegalMoniterStateException();
    }
    boolean free = false;
    if(c == 0){
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);  //释放锁的最后,写 volatile 变量 state
    return free;
}

从上面的源代码可以看出,在释放锁的最后写 volatile 变量 state

公平锁在释放锁的最后写 volatile 变量 state,在获取锁时首先读这个 volatile 变量。根据 volatile 的 happends-before 规则,释放锁的线程在写 volatile 变量之前可见的共享变量,在获取锁的线程读取同一个 volatile 变量后将立即变得对获取锁的线程可见。

非公平锁的释放和公平锁完全一样,所以这里仅仅分析非公平锁的获取。

使用非公平锁时,加锁方法 lock() 调用轨迹如下。

  1. ReetrantLock: lock()。
  2. NonfairSync: lock()。
  3. AbstractQueueSynchronizer:compare AndSetState(int expect,int update)。

在第 3 步真正开始加锁,下面时该方法的源代码

protected final boolean compareAndSetState(int expect,int update){
     return unsafe.compareAndSwapInt(this,stateoffset,expect,update);
}

该方法以原子操作的方式更新 state 变量,本文把 Java 的 compareAndSet() 方法调用简称为 CAS 。 JDK 文档对该方法的说明如下:如果当前状态等于预期值,则以原子方式将同步状态设置为给定的更新值。

现在对公平锁 和非公平锁的的内存语义做个总结

  • 公平锁和非公平锁释放时,最后都要写一个 volatile 变量 state
  • 公平锁释放时,首先会去读 volatile 变量
  • **非公平锁获取时,首先会用 CAS 更新 volatile 变量,这个操作同时具有 volatile 读和 volatile 写的内存语义。

从以上分析可以看出。锁释放-获取的内存语义的实现至少有下面两种方式。

  1. 利用 volatile 变量的写-读锁具有的内存语义
  2. 利用 CAS 所附带的 volatile 读 和 volatile 写的内存语义

4.concurent 包的实现

由于 Java 的 CAS 同时具有 volatile 读和 volatile 写的内存语义,因此 Java 线程之间的通信现在有了 下面 4 种方式。

  1. A 线程写 volatile 变量,随后 B 线程读这个 volatile 变量。
  2. A 线程写 volatile 变量,随后 B 线程利用 CAS 更新这个 volatile 变量。
  3. A 线程利用 CAS 更新一个 volatile 变量,随后 B 线程用 CAS 更新这个 volatile
    变量。
  4. A 线程用 CAS 更新一个 volatile 变量,随后 B 线程读这个 volatile 变量。

仔细分析 concurrent 包的源代码实现,会发现一个通用化的实现模式。

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

happens-before

happens-before 是 JMM 最核心的概念。对应 Java 程序员来说,理解 happends-before 是理解 JMM 的关键

1.JMM 的设计

从 JMM 设计者的角度,在设计 JMM 时,需要考虑两个关键因素

  • 程序员对内存模型的使用。程序员希望内存模型易于理解、易于编程。程序员希望基于一个强内存模型来编写代码。
  • 编译器和处理器对内存模型的实现。编译器和处理器希望内存模型对它们的束缚越少越好,这样它们就可以尽可能的优化来提高性能。编译器和处理器希望实现一个弱内存模型。

所以 JSR-133 专家组在设计 JMM 时的核心目标就是找到一个好的平衡点: 一方面,要为程序员提供足够强的内存可见性保证;另一方面,对编译器和处理器的限制要尽可能地放松。

JMM 把 happens-before 要求禁止的 重排序分为下面两类。

  • 会改变程序执行结果的重排序。
  • 不会改变程序执行结果的重排序

JMM 对着两种不同性质的重排序,采用了不同的策略,如下。

  • 对于会改变程序执行结果的重排序,JMM 要求编译器和处理器必须禁止这种重排序
  • 对于不会改变程序执行结果的重排序, JMM 对编译器和处理器不做要求*(JMM 允许这种重排序)。

JMM 其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。

2.happens-before 的定义

  1. 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

  2. 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按 happens-before 关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM 允许这种重排序)。

3. happens-before 规则

  1. 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。

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

  3. volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。

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

  5. start() 规则: 如果线程 A 执行结果 ThreadB.start() (启动线程 B),那么 A 线程中的 ThreadB.start() 操作 happens-before 于线程 B 中的任意操作。

  6. join() 规则:如果线程 A 执行操作 ThreadB.join() 并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join() 操作成功返回。

双重检查锁定与延迟初始化

1. 双重检查锁定的由来

在 Java 程序中,有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化

所以程序员可能会采用延迟初始化。但是正确使用线程安全的延迟初始化需要一些技巧,否则很容器出现问题。

public class UnsafeLazyInitialization{
    private static Instance instance;

     public static Instance getInstance(){
         if(instance == null){
             instance = new Instance();    // 1: A线程执行
         } 
         return instance;                        // 2:   B线程执行
      }
}

在 UnsafeLazyInitialization 类中,假设 A 线程执行代码 1 的同时,B 线程执行代码 2.此时,线程 A 可能看到 instance 引用的对象还没有完成初始化。

我们可以对 getInstance() 方法做同步处理来实现线程安全的 延迟初始化。

public class SafeLazyInitialization{
     private static Instance instance;
     public synchronized static Instance getInstance(){
         if(instance == null){
              instance = new Instance();
         }
          return instance;
     }
}

由于对 getInstance() 方法做了同步处理,synchronized 将导致性能开销。如果 getInstance() 方法被多个线程频繁的调用,将会导致程序执行性能的下降。反之,如果 getInstance() 方法不会被多个线程频繁的调用,那么这个延迟初始化方案将能提供令人满意的性能。

在早期的 JVM 中,synchronized (甚至是无竞争的 synchronized)存在巨大的性能开销。因此人们想出了一个 "聪明"的技巧:双重检查锁定

public class DoubleCheckedLocking{ //1
   
   private static Instance instance; //2
   
   public static Instance getInstance(){  //3
        if(instance == null){                //4     
              synchronized (DoubleCheckedLocking.class){  //5
                  if(instance == null){       //6
                      instance = new Instance();   //7
                  }
              }
        }
   }
}

如果第一次检查 instance 不为 null,那么就不需要执行下面的加锁和初始化操作。因此,可以大幅降低 synchronized 带来的性能开销。

  • 多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。
  • 在对象创建好之后,执行 getInstance() 方法不需要获取锁,直接返回已创建好的对象。

双重检查锁定看起来似乎很完美,但这是一个错误的优化!在程序执行到第 4 行,代码读取到 instance 不为 null 时,instance 引用的对象很有可能还没有完成初始化。

2. 问题的根源

前面的双重检查锁定示例代码的第 7 行(instance = new Instance();) 创建了一个对象。这一行代码可以分解为如下的 3 行伪代码。

  memory = allocate();    // 1:分配对象的内存空间
  ctorInstance(memory);  // 2:  初始化对象
  instance  = memory;     // 3: 设置instance 指向刚分配的内存地址

上面 3 行 伪代码中的 2 和 3 之间,可能会被重排序。2 和 3 之间重排序之后的执行结果如下。

  memory = allocate();  //1.分配对象内存空间
  instance = memory;    //3. 设置 instance 指向刚分配的内存地址
  ctorInstance(memory); //2.初始化对象

DoubleCheckedLocking 示例代码 的 第 7 行(instance = new Singleton();) 如果发生重排序,另一个并发执行的线程 B 就有可能在 第 4 行判断 instance 不为 null。 线程 B 接下来将访问 instance 锁引用的对象,但此时这个对象可能还没有被 A 线程初始化!

在知晓问题根源之后,可以想出两个办法来实现线程安全的延迟初始化。

  1. ** 不允许 2 和 3 重排序**
  2. 允许 2 和 3 重排序,但不允许其他线程"看到"这个重排序。

3. 基于 volatile 的解决方案

只需要做一点小的修改(把 instance 声明为 colatile 型),就可以实现线程安全的延迟初始化。

public class SafeDoubleCheckedLocking{
    private volatile static Instance instance;
    
    public static Instance getInstance(){
        if(instance == null ){
             synchronized(SafeDoubleCheckedLocking.class){
                 if(instance == null){
                      instance = new Instance();   //instance 为 volatile,现在没问题了
                 }
             }
        }
    }
}

这个解决方案需要 JDK5 或更高版本(因为从 JDK 5 开始使用新的 JSR-133 内存模型规范,这个规范增强了 volatile 的语义)。

当声明对象的引用为 volatile 后, 2 和 3 之间的重排序,在多线程环境中将会被禁止。

4. 基于类初始化的解决方案

JVM 在类的初始化阶段(即在 Class 被加载后,且被线程使用之前),会执行类的初始化。
在执行类的初始化期间, JVM 会去获取一个锁,这个锁可以同步多个线程对同一个类的初始化

基于这个特性,可以实现另一种线程安全的延迟初始化方案 (这个方案称之为 Initialization On Demand Holder idiom)

public class InstanceFactory{
     private static class InstanceHolder{
          public static Instance instance = new Instance();
     }
     public static Instance getInstance(){
          return InstanceHolder.instance;   // 这里将导致 InstanceHolder 类被初始化
    }
}

初始化一个类,包括执行这个类的静态初始化和初始化这个类中声明的静态字段。根据 Java 语言规范,在首次发生下列任意一种情况时,一个类或接口类型 T 将被立即初始化。

**1) T 是一个类,而且一个 T 类型的实例将被创建。

  1. T 是一个类,且 T 中声明的一个静态方法被调用。
  2. T 中声明的一个静态字段被赋值。
  3. T 中声明的一个静态字段被使用,而这个字段不是一个常量字段。
  4. T 是一个顶级类(Top Level Class),而且一个断言语句嵌套在 T 内部被执行。

在 上面代码中,首次执行 getInstance() 方法的线程将导致 InstanceHolder 类被初始化

Java 内存模型综述

1. 处理器的内存模型

顺序一致性内存模型是一个理论参考模型,JMM 和处理器内存模型在设计时通常会以顺序一致性内存模型作为参考。

如果完全按照顺序一致性模型来实现处理器和 JMM ,那么很多的处理器和编译器优化都要被禁止,这对执行性能将会有很大影响。

根据对不同类型的 读/写操作组合的执行顺序的放松,可以把常见处理器的内存模型划分为如下几种类型。

  • 放松程序中写-读操作的顺序,由此产生了 Total Store Ordering 内存模型(简称 TSO)。
  • 在上面的基础上,继续放松程序中 写- 写操作的顺序,由此产生了 Partial Store Order 内存模型 (简称 PSO)。
  • 在前面两条的基础上,继续放松程序中读-写和读-读操作的顺序,由此产生了 Relaxed Memory Order 内存模型(简称 RMO) 和 PowerPC 内存模型。

注意,这里处理器对读/写操作的放松,是以两个操作之间不存在数据依赖性作为前提的(因为处理器要遵守 as-if-serial 语义,处理器不会对存在数据依赖性的两个内存操作做重排序)。

越是追求性能的处理器,内存模型设计得会越弱。他们呢就可以做尽可能多得优化来提高性能。

JMM 屏蔽了不同处理器内存模型得差异,它在不同得处理器平台之上为 Java 程序员呈现了一个一致得内存模型。

2.各种内存模型之间的关系

同处理器内存模型一样,越是追求执行性能的语言,内存模型设计得会越弱。

3. JMM 的内存可见性保证

按程序类型,Java 程序的内存可见性保证可分为以下 3 类。

  • 单线程程序。 单线程程序不会出现内存可见性问题。
  • 正确同步的多线程程序。正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与程序在顺序一致性内存模型中的执行结果相同)。这是 JMM 关注的重点,JMM 通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。
  • 未同步/未正确同步的多线程程序。JMM 为它们提供了最小安全性保障:线程执行时读取的到的值,要么是之前某个线程吸入的值,要么是默认值(0、null、false)。

4. JSR-133 对旧内存模型的修补

JSR-133 对 JDK5 之前的旧的内存模型的修补主要有两个。、

  • 增强 volatile 的内存语义旧内存模型允许 volatile 变量与普通变量的重排序。JSR-133 严格限制 volatile 变量与普通变量的重排序,使 volatile 的写-读 和锁的释放-获取具有相同的内存语义。
  • 增强 final 的内存语义多次读取同一个 final 变量的值可能会不相同

标题:Java 内存模型
作者:zh847707713
地址:http://lovehao.cn/articles/2020/03/07/1583516587481.html