并发编程之Java内存模型

目录
  • 一、Java内存模型的基础
    • 1.1 并发编程模型的两个关键问题
    • 1.2 Java内存模型的抽象结构
    • 1.3 从源代码到指令重排序
    • 1.4 写缓冲区和内存屏障
      • 1.4.1 写缓冲区
      • 1.4.2 内存屏障
    • 1.5 happens-before 简介

简介:

Java线程之间的通信对程序员完全透明,内存可见性问题很容易困扰Java程序员,这一系列几篇文章将揭开Java内存模型的神秘面纱。

这一系列的文章大致分4个部分,分别是:

  • Java内存模型基础,主要介绍内存模型相关基本概念
  • Java内存模型中的顺序一致性,主要介绍重排序与顺序一致性内存模型
  • 同步原语,主要介绍三个同步原语(synchronizedvolatile和final)的内存语义及重排序规则在处理器中的实现
  • Java内存模型的设计,主要介绍Java内存模型的设计原理,及其与处理器内存模型和顺序一致性内存模型的关系。

一、Java内存模型的基础

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

在并发编程中需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体)。

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

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

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

  • 共享内存:同步是显式进行的,由于程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行
  • 消息传递:同步是隐式进行的,由于消息的发送必须在消息的接收之前。

总结:

Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明,如果编写多线程程序的Java程序员不理解隐式进行线程之间的通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。

1.2 Java内存模型的抽象结构

Java中所有的实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享(文章中用“共享变量”指代)。局部变量(Local Variables)、方法定义参数(Formal Method Parameters)和异常处理器参数(Exception Handler Parameters)不会在线程之间共享,它们不会存在内存可见性问题,因此也不受内存模型的影响。
Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存时JMM的一个抽象概念,并不真实存在。JMM涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

图示:Java内存模型的抽象示意图

从上图来看,线程A和线程B之间要通信的话,必须经历下面2个步骤。

  • 线程A把本地内存A中更新过的变量刷新到主内存中
  • 线程B到主内存中去读取线程A之前已更新过的共享变量

图示:线程之间通信示意图

如上图所示,本地内存A和本地内存B有主内存中共享变量X的副本。假设初始时,这三个内存中的X的值都是0.线程A在执行时,把更新后的X的值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信是,线程A首先把自己本地内存中修改后的X刷新到主内存中,此时主内存中的X值变为了1.随后,线程B到主内存中去读取线程A更新后的X值,此时线程B的本地内存X的值也更新成了1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。

1.3 从源代码到指令重排序

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

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

从Java源代码的最终实际执行的指令序列,会分别经历下面3种重排序,其中1属于编译器重排序,2和3属于处理器重排序。

源代码到最终执行的指令序列示意图:

重排序可能会导致多线程程序出现内存可见性问题,对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都需要禁止)。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barries, Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。
JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保障。

1.4 写缓冲区和内存屏障

1.4.1 写缓冲区

现代处理器都会使用写缓冲区临时保存向内存中写入的数据。写缓冲区的主要作用:

  • 可以保证指令流水线持续运行,可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。
  • 它以批处理的方式方式刷新写缓冲区,以及合并写缓冲区中对统一地址的多次写,减少对内存总线的占用。

常见处理器允许的重排序类型(Y-表示允许两个操作重排序,N-表示处理器不允许两个操作重排序)

处理器 \规则 Load-Load Load-Store Store-Store Store-Load 数据依赖性
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

说明:常见处理器都允许Store-Load重排序;常见的处理器都不允许对存在数据依赖性的操作做重排序。N多的表示处理器拥有相对较强的处理器内存模型。

由于写缓冲器仅仅只对它所在的处理器可见,这个特性会对内存操作的执行顺序产生非常重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致。

举例说明:

示例项目 \处理器 Processor A Processor B
伪代码 a=1; //A1x=b;//A2 b=2;//B1y=a;//B2
可能运行结果 初始状态:a=b=0;处理器允许执行后得到结果:x=y=0;

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

处理器和内存交互:

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

1.4.2 内存屏障

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

JMM把内存屏障指令分为4类:

屏障类型 指令示例 说明
LoadLoad Barriers Load1;LoadLoad;Load2 确保Load1数据的装载先于Load2及所有后续装载指令的装载
StoreStore Barriers Store1;StoreStore;Store2 确保Store1数据对其他处理器可见(刷新到主内存)先于Store2及所有后续存储指令的存储
LoadStore Barriers Load1;LoadStore;Store2 确保Load1数据装载先于Store2及后续的存储指令刷新到内存
StoreLoad Barriers**** Store1;StoreLoad;Load2 确保Store1数据对其他处理器变得可见(指刷新到主内存)先于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行屏障之后的内存访问指令。

StoreLoad Barriers是一个“全能型屏障”,它同时具有其它3个屏障的效果。现代大多数处理器支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为处理器需要把缓冲区的内容全部刷新到内存中(Buffer Fully Flush)。

1.5 happens-before 简介

从JDK1.5开始,Java使用新的JSR-133内存模型。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。这里的两个操作可以是单线程也可以是多线程。

happens-before规则:

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

注意:

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

图示happens-before与JMM的关系:

一个happens-before规则对应于一个或多个编译器个处理器重排序规则。对于Java程序员来说,happens-before规则简单易懂,它避免了Java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法。

到此这篇关于Java并发编程之内存模型的文章就介绍到这了,更多相关Java内存模型内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 并发编程之Java内存模型顺序一致性

    目录 1.数据竞争和顺序一致性 1.1 Java内存模型规范对数据竞争的定义 1.2 JMM对多线程程序的内存一致性做的保证 2.顺序一致性内存模型 2.1 特性 2.2 举例说明顺序一致性模型 2.3 同步程序的顺序一致性效果 2.4 未同步程序的执行特性 3. 64位long型和double型变量写原子性 3.1 CPU.内存和总线简述 3.2 long和double类型的操作 简介: 顺序一致性内存模型是一个理论参考模型,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参照

  • Java并发编程之Java内存模型

    目录 1.什么是Java的内存模型 2.为什么需要Java内存模型 3.Java内存模型及操作规范 4.Java内存模型规定的原子操作 5.Java内存模型同步协议 6.Java内存模型的HB法则 JMM的HB法则 总结 1.什么是Java的内存模型 Java内存模型简称JMM(Java Memory Model),JMM是和多线程并发相关的一组规范.各个jvm实现都要遵循这个JMM规范.才能保证Java代码在不同虚拟机顺利运行.因此,JMM 与处理器.缓存.并发.编译器有关.它解决了CPU 多

  • 并发编程之Java内存模型volatile的内存语义

    1.volatile的特性 理解volatile特性的一个好办法是把对volatile变量的单个读/写,看成是使用同一个锁对单个读/写操作做了同步. 代码示例: package com.lizba.p1; /** * <p> * volatile示例 * </p> * * @Author: Liziba * @Date: 2021/6/9 21:34 */ public class VolatileFeatureExample { /** 使用volatile声明64位的long型

  • 并发编程之Java内存模型锁的内存语义

    目录 1.锁的释放-获取建立的happens-before关系 2.锁释放和获取的内存语义 3.锁内存的语义实现 4.concurrent包的实现 简介: 锁的作用是让临界区互斥执行.本文阐述所得另一个重要知识点--锁的内存语义. 1.锁的释放-获取建立的happens-before关系 锁是Java并发编程中最重要的同步机制.锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息. 锁释放-获取的示例代码: package com.lizba.p1; /** * <p>

  • 并发编程之Java内存模型

    目录 一.Java内存模型的基础 1.1 并发编程模型的两个关键问题 1.2 Java内存模型的抽象结构 1.3 从源代码到指令重排序 1.4 写缓冲区和内存屏障 1.4.1 写缓冲区 1.4.2 内存屏障 1.5 happens-before 简介 简介: Java线程之间的通信对程序员完全透明,内存可见性问题很容易困扰Java程序员,这一系列几篇文章将揭开Java内存模型的神秘面纱. 这一系列的文章大致分4个部分,分别是: Java内存模型基础,主要介绍内存模型相关基本概念 Java内存模型

  • Java 高并发三:Java内存模型和线程安全详解

    网上很多资料在描述Java内存模型的时候,都会介绍有一个主存,然后每个工作线程有自己的工作内存.数据在主存中会有一份,在工作内存中也有一份.工作内存和主存之间会有各种原子操作去进行同步. 下图来源于这篇Blog 但是由于Java版本的不断演变,内存模型也进行了改变.本文只讲述Java内存模型的一些特性,无论是新的内存模型还是旧的内存模型,在明白了这些特性以后,看起来也会更加清晰. 1. 原子性 原子性是指一个操作是不可中断的.即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其它线程干扰

  • Java内存模型final的内存语义

    目录 1.final域的重排序规则final 2.写final域的重排序规则 3.读final与的重排序规则 4.final域为引用类型 5.为什么final引用不能从构造函数内"逸出" 6.final语义在处理器中的实现 7.JSR-133为什么要增强final的语义 上篇并发编程之Java内存模型volatile的内存语义介绍了volatile的内存语义,本文讲述的是final的内存语义,相比之下,final域的读和写更像是普通变量的访问. 1.final域的重排序规则final

  • Java并发编程之volatile与JMM多线程内存模型

    目录 一.通过程序看现象 二.为什么会产生这种现象(JMM模型)? 三.MESI 缓存一致性协议 一.通过程序看现象 在开始为大家讲解Java 多线程缓存模型之前,我们先看下面的这一段代码.这段代码的逻辑很简单:主线程启动了两个子线程,一个线程1.一个线程2.线程1先执行,sleep睡眠2秒钟之后线程2执行.两个线程使用到了一个共享变量shareFlag,初始值为false.如果shareFlag一直等于false,线程1将一直处于死循环状态,所以我们在线程2中将shareFlag设置为true

  • Java并发编程之Volatile变量详解分析

    目录 一.volatile变量的特性 1.1.保证可见性,不保证原子性 1.2.禁止指令重排 二.内存屏障 三.happens-before Volatile关键字是Java提供的一种轻量级的同步机制.Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量, 相比synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度. 但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其

  • 浅谈Java并发编程之Lock锁和条件变量

    简单使用Lock锁 Java 5中引入了新的锁机制--java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作.Lock接口有3个实现它的类:ReentrantLock.ReetrantReadWriteLock.ReadLock和ReetrantReadWriteLock.WriteLock,即重入锁.读锁和写锁.lock必须被显式地创建.锁定和释放,为了可以使用更多的功能,一般用ReentrantLock为其实例

随机推荐