并发编程-Java内存模型到底是什么

内存模型

在计算机CPU,内存,IO三者之间速度差异,为了提高系统性能,对这三者速度进行平衡。

  • CPU 增加了缓存,以均衡与内存的速度差异;
  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。

以上三种系统优化,对于硬件的效率有了显著的提升,但是他们同时也带来了可见性,原子性以及顺序性等问题。基于Cpu高速缓存的存储交互很好得解决了CPU和内存得速度矛盾,但是也提高了计算机系统得复杂度,引入了新的问题:缓存一致性(Cache Coherence)。

每个处理器都有自己独享得高速缓存,多个处理器共享系统主内存,当多个处理器运算任务涉及到同一块主内存区域时,将可能会导致数据不一致,这时以谁的数据为准就成了问题。为了解决一致性问题,各个处理器需要遵守一些协议,根据这些协议来进行读写操作。所以内存模型可以理解为是为了解决缓存一致性问题,在特定的操作协议下,对特定的内存或高速缓存进行读写的过程的抽象。

Java内存模型

JMM的作用

Java虚拟机规范试图定义一种Java内存模型(Java Memory Model, JMM),用来屏蔽掉硬件和操作系统的内存访问差异,以实现让Java程序在各种平台都能达到一致的内存访问效果。使得Java程序员可以忽略不同处理器平台的不同内存模型,而只需要关心JMM即可。

JMM抽象结构

JMM 抽象结构图

JMM借鉴了处理器内存模型的思想,从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系,它涵盖了缓存,写缓冲区,寄存器以及其他硬件和编译器优化。下图是JMM的抽象结构示意图。

JMM中线程间通信

并发编程中需要考虑的两个核心问题:线程之间如何通信(可见性和有序性)以及线程之间如何同步(原子性)。通信是指线程之间以何种方式进行信息交换;同步是指程序中用于控制不同线程间操作发生的相对顺序

JMM规定了程序中所有的变量(实例字段,静态字段,构成数组对象的元素等)都存储在主内存中;它的主要目标是定义程序种各个变量的访问规则,既从虚拟机将变量存储到内存和从内存种取出变量这样的底层细节。每个线程都有自己的本地内存,线程之间在JMM控制协议的限制下通过主内存进行通信。假设由两个线程A和B,线程A要给线程B发送"hello"消息,下图是两个线程进行通信的过程:

由图可见,假设线程A要发消息给线程B,那么它必须经过两个步骤:

  1. 线程A把本地内存中的共享变量副本message更新后刷新到主内存中
  2. 线程B到主内存取读取线程A更新的共享变量message

JMM的设计与实现

JMM相关的协议比较复杂,我们可以从编译器或者JVM工程师,以及Java工程师来进行学习。本文仅从Java工程师角度来进行探讨Java中通过那些协议来控制JMM,从而保证数据一致性。

JMM的实现可以分为两部分,包括happen-before规则以及一系列的关键字。它的核心目标就是确保编译器,各平台的处理器都能提供一致的行为,在内存中表现出一致性的结果。具体来讲就是通过happens-before规则以及volatile,synchronized,final关键字解决可见性,原子性以及有序性问题,从而保证内存中数据的一致性。

Happens-Before规则

happens-before是JMM中最核心的概念,happens-before用来指定两个操作之间的执行顺序,这两个操作可以在一个线程内,也可以在不同的线程内,因此JMM通过happen-before关系向程序员提供跨线程的内存可见性保证,JMM的具体定义如下:

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

下面的示例代码,假设线程 A 执行 writer() 方法,线程 B 执行 reader() 方法,如果线程 B 看到 “v == true” 时,那么线程 B 看到的变量 x 是多少呢?

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;                 // 1
    v = true;               // 2
  }
  public void reader() {
    if (v == true) {        // 3
      // 这里 x 会是多少呢?  // 4
    }
  }
}

1. 程序顺序性规则

程序顺序规则(Program Order Rule): 一个线程内的每个操作,按照代码先后顺序,书写在前面的代码先行发生于与写在后面的操作。

2. volatile变量规则

volatile变量规则(Volatile Variable Rule):对于一个volatile修饰得变量得写操作先行发生于后面对这个变量得读操作。“后面”指得是时间上的顺序

3. 传递性规则

传递性规则(Transitivity): 如果操作A先行发生于操作B, 操作B先行发生于操作C,那么A先行发生于操作C。

针对上述的1,2,3项happens-before我们作出个总结,下图是我们根据volatile读写建立的happens-before关系图。

4. 程锁定规则

管程锁定规则(Monitor Lock Rule): 一个unlock操作先行发生于后面对这个锁得lock操作。“后面”指得是时间上的顺序

在之前文章并发问题的源头中并发问题中count++的问题提到了线程切换导致计数出现问题,在此我们就可以尝试利用happens-before规则解决这个原子性问题。

public class SafeCounter {
  private long count = 0L;
  public long get() {
    return cout;
  }
  public synchronized void addOne() {
    count++;
  }
}

上述代码真的解决可以解决问题吗?

4. 线程启动规则

线程启动规则(Thread Start Rule): Thread对象的start()方法,先行发生于此线程的每一个动作。

6.线程终止规则

线程终止规则(Thread Termination Rule): 线程中的所有操作都先行发生于对于此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()返回值等手段来检测线程是否执行完毕。

7. 线程中断规则

线程中断规则(Thread Interruption Rule): 对线程的interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

8. 对象终结规则

对象终结规则(Finalizer Rule): 一个对象的初始化完成(构造函数执行完毕)先行发生于它的finalize()方法。

happens-before规则一共可分为以上8条,笔者只针对在并发编程中常见的前6项进行了详细介绍,具体内容可以参考http://gee.cs.oswego.edu/dl/jmm/cookbook.html。在JMM中,我认为这些规则也是比较难以理解的概念。总结下来happens-before规则强调的是一种可见性关系,事件A happens-before B,意味着A事件对于B事件是可见的,无论事件A和事件B是否发生在一个线程里。

volatile关键字

volatile自身特性

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

volatile在JMM中表现出的内存语义

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

volatile是java中提供用来解决可见性问题得关键字,可以理解为jvm看见volatile关键字修饰的变量时,会“禁用缓存”既线程的本地内存,每次对此类型变量的读操作时都会从主内存中重新读取到本地内存中,每次写操作也会立刻同步到主内存中,这也正进一步诠释了volatile变量规则中描述的,对于一个volatile修饰得变量得写操作先行发生于后面对这个变量得读操作;被volatile修饰的共享变量,会被禁用某些类型的指令重排序,来保证顺序性问题。

synchronized-万能的锁

由管程锁定规则,一个unlock操作先行发生于后面对这个锁的lock操作。在Java中通过管程(Monitor)来解决原子性问题,具体的表现为Synchronized关键字。被synchronized修饰的代码块在编译时会在开始位置和结束位置插入monitorenter和monitorexit指令,JVM保证monitorenter和monitorexit与之与之配对,并且这段代码得原子性。synchronized中的lock和unlock操作是隐式进行的,在java中我们不仅可以使用synchronized关键字,同样可以使用各种实现了Lock接口的锁来实现。

synchronized的内存语义

  1. 当线程获取锁时,会把线程本地内存置为无效
  2. 当线程释放锁时,会将共享变量刷新到主内存中

final-默默无闻的优化

在并发编程中的原子性,可见性以及顺序性的问题导致的根本就是共享变量的改变。final关键字解决并发问题的方式是从源头下手,让变量不可变,变量被final修饰表示当前变量不会发生改变,编译器可以放心进行优化。

总结

  1. JMM是用来屏蔽掉硬件和操作系统的内存访问差异,以实现让Java程序在各种平台都能达到一致的内存访问效果
  2. 站在称序员角度来看JMM是一系列的协议(hanppens-before规则)和一些关键字,Synchronized,volatile和final
  3. volatile通过禁用缓存和编译优化保证了顺序性和可见性
  4. synchronzed能保证程序执行的原子性,可见性和有序性,是并发中的万能要是
  5. final关键字修饰的变量 不可变

Q&A

上文中尝试用synchronized解决count++的问题,为了方便观察将代码copy到此处,这段代码有没有什么不对劲呢?可以在留言区说出你的想法,我们一起来学习!

public class SafeCounter {
  private long count = 0L;
  public long get() {
    return cout;
  }
  public synchronized void addOne() {
    count++;
  }
}

笔者的个人博客网站

原文地址:https://www.cnblogs.com/liqiangchn/p/11735930.html

时间: 2024-11-06 07:12:21

并发编程-Java内存模型到底是什么的相关文章

并发编程-Java内存模型:解决可见性与有序性问题

背景 我们知道导致cpu缓存导致了可见性问题,编译器优化带来了有序性问题.那么如果我们禁用了cpu缓存与编译器优化,就能够解决问题,但是性能就无法提升了.所以一个合理的方案,就是按照一定规范来禁用缓存和编译器优化,即在某些情况下禁用缓存与编译器优化.Java内存模型就是这样的一个规范,用来解决可见性与有序性问题 概念 java内存模型本质上就是规范了JVM如何按照规则禁用缓存和编译器优化,既面向应用开发人员,也面向jvm的实现.这些规范包括了volatile.synchronized和final

【死磕Java并发】-----Java内存模型之分析volatile

前篇博客[死磕Java并发]-–深入分析volatile的实现原理 中已经阐述了volatile的特性了: volatile可见性:对一个volatile的读,总可以看到对这个变量最终的写: volatile原子性:volatile对单个读/写具有原子性(32位Long.Double),但是复合操作除外,例如i++; JVM底层采用"内存屏障"来实现volatile语义 下面LZ就通过happens-before原则和volatile的内存语义两个方向介绍volatile. volat

JAVA多线程编程——JAVA内存模型

一.何为"内存模型" 内存模型描述了程序中各个变量(实例域.静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存中取出变量这样的底层细节,对象最终是存储在内存里面的,但是编译器.运行库.处理器或者系统缓存可以有特权在变量指定内存位置存储或者取出变量的值. 二.JMM(Java Memory Model)即Java内存模型的作用 JMM的最初目的是为了能够支持多线程程序.JMM使得每一个线程就像运行在不同的机器.不同的CPU或者本身就不同的线程上一样: JMM定义

并发艺术--java内存模型

前言 本章大致分为四部分. java内存模型的基础,介绍内存模型的相关基本概念: java内存模型中的顺序一致性,主要介绍重排序和顺序一致性: 同步原语,涉及synchronized,volatile,final三个同步原语的内存含义及重排序等: java内存模型的设计,涉及与内存模型和顺序一致性内存模型关系. 一.java内存模型基础  1.1 并发编码模型的两个关键问题--线程是并发执行的活动实体 线程之间如何通信 共享内存 - 通过写-读 内存中的公共状态进行隐式通信,java采用的是共享

Java内存模型分析

在学习Java内存模型之前,先了解一下线程通信机制. 1.线程通信机制 在并发编程中,线程之间相互交换信息就是线程通信.目前有两种机制:内存共享与消息传递. 1.1.共享内存 Java采用的就是共享内存,本次学习的主要内容就是这个内存模型. 内存共享方式必须通过锁或者CAS技术来获取或者修改共享的变量,看起来比较简单,但是锁的使用难度比较大,业务复杂的话还有可能发生死锁. 1.2.消息传递 Actor模型即是一个异步的.非阻塞的消息传递机制.Akka是对于Java的Actor模型库,用于构建高并

《Java并发编程实战》第十六章 Java内存模型 读书笔记

Java内存模型是保障多线程安全的根基,这里仅仅是认识型的理解总结并未深入研究. 一.什么是内存模型,为什么需要它 Java内存模型(Java Memory Model)并发相关的安全发布,同步策略的规范.一致性等都来自于JMM. 1 平台的内存模型 在架构定义的内存模型中将告诉应用程序可以从内存系统中获得怎样的保证,此外还定义了一些特殊的指令(称为内存栅栏或栅栏),当需要共享数据时,这些指令就能实现额外的存储协调保证. JVM通过在适当的位置上插入内存栅栏来屏蔽在JVM与底层平台内存模型之间的

Java并发编程实战 第16章 Java内存模型

什么是内存模型 JMM(Java内存模型)规定了JVM必须遵循一组最小保证,这组保证规定了对变量的写入操作在何时将对其他线程可见. JMM为程序中所有的操作定义了一个偏序关系,称为Happens-Before.两个操作缺乏Happens-Before关系,则Jvm会对它们进行任意的重排序. Happends-Before的规则包括: 1. 程序顺序规则.若程序中操作A在操作B之前,则线程中操作A在操作B之前执行. 2. 监视器锁规则.在同一监视器锁上的解锁操作必须在加锁操作之前执行.如图所示,

《java并发编程的艺术》读书笔记-第三章Java内存模型(二)

一概述 本文属于<java并发编程的艺术>读书笔记系列,第三章java内存模型第二部分. 二final的内存语义 final在Java中是一个保留的关键字,可以声明成员变量.方法.类以及本地变量.可以参照之前整理的关键字final.这里作者主要介绍final域的内存语义. 对于final域,编译器和处理器要遵守两个重排序规则: 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序. 初次读一个包含final域的对象的引用,与随后初次读这

&lt;java并发编程的艺术&gt;读书笔记-第三章java内存模型(一)

一概述 本文属于<java并发编程的艺术>读书笔记系列,继续第三章java内存模型. 二重排序 2.1数据依赖性 如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性.数据依赖分下列三种类型: 名称 代码示例 说明 写后读 a = 1;b = a; 写一个变量之后,再读这个位置. 写后写 a = 1;a = 2; 写一个变量之后,再写这个变量. 读后写 a = b;b = 1; 读一个变量之后,再写这个变量. 上面三种情况,只要重排序两个操作的执行顺序,