内存模型

在计算机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++;
}
}

笔者的个人博客网站

我要学并发-Java内存模型到底是什么的更多相关文章

  1. Java并发-Java内存模型(JMM)

    先来说说什么是内存模型吧 在硬件中,由于CPU的速度高于内存,所以对于数据读写来说会出现瓶颈,无法充分利用CPU的速度,因此在二者之间加入了一个缓冲设备,高速缓冲寄存器,通过它来实现内存与CPU的数据 ...

  2. Java高并发-Java内存模型和线程安全

    一.原子性 原子性是指一个操作是不可中断的.即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其它线程干扰. i++是原子操作吗? 不是,包含3个操作:读i,i=i+1,写i 32位的机子上读取 ...

  3. 并发编程之 Java 内存模型 + volatile 关键字 + Happen-Before 规则

    前言 楼主这个标题其实有一种作死的味道,为什么呢,这三个东西其实可以分开为三篇文章来写,但是,楼主认为这三个东西又都是高度相关的,应当在一个知识点中.在一次学习中去理解这些东西.才能更好的理解 Jav ...

  4. Java内存模型JMM 高并发原子性可见性有序性简介 多线程中篇(十)

    JVM运行时内存结构回顾 在JVM相关的介绍中,有说到JAVA运行时的内存结构,简单回顾下 整体结构如下图所示,大致分为五大块 而对于方法区中的数据,是属于所有线程共享的数据结构 而对于虚拟机栈中数据 ...

  5. 【Java虚拟机4】Java内存模型(硬件层面的并发优化基础知识--缓存一致性问题)

    前言 今天学习了Java内存模型第一课的视频,讲了硬件层面的知识,还是和大学时一样,醍醐灌顶.老师讲得太好了. Java内存模型,感觉以前学得比较抽象.很繁杂,抽象. 这次试着系统一点跟着2个老师学习 ...

  6. Java内存模型---并发编程网 - ifeve.com

    Java内存模型 转自:http://ifeve.com/java-memory-model-6/ 原文地址  作者:Jakob Jenkov 译者:张坤 Java内存模型规范了Java虚拟机与计算机 ...

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

    Java内存模型是保障多线程安全的根基,这里不过认识型的理解总结并未深入研究. 一.什么是内存模型,为什么须要它 Java内存模型(Java Memory Model)并发相关的安全公布,同步策略的规 ...

  8. JVM-7.Java内存模型与高效并发

    更多内容参见<并发与同步>系列 一.引子 二.JMM 三.Java中的线程 四.线程安全 五.锁优化       一.引子 运算能力 摩尔定律:晶体管数量,代表的CPU的频率 Amdahl ...

  9. Java并发编程:JMM(Java内存模型)和volatile

    1. 并发编程的3个概念 并发编程时,要想并发程序正确地执行,必须要保证原子性.可见性和有序性.只要有一个没有被保证,就有可能会导致程序运行不正确. 1.1. 原子性 原子性:即一个或多个操作要么全部 ...

随机推荐

  1. 1、Spark 2.1 源码编译支持CDH

    目前CDH支持的spark版本都是1.x, 如果想要使用spark 2x的版本, 只能编译spark源码生成支持CDH的版本. 一.准备工作 找一台Linux主机, 由于spark源码编译会下载很多的 ...

  2. 软件测试的分类&软件测试生命周期

    软件测试的分类: 按测试执行阶段:单元测试.集成测试.系统测试.验收测试.(正式验收测试,Alpha 测试-内侧,Beta 测试-公测) 按测试技术分类:黑盒测试.白盒测试.灰盒测试 按测试对象是否运 ...

  3. linux双网卡绑定为逻辑网卡

    网卡bond是通过多张网卡绑定为一个逻辑网卡,实现本地网卡的冗余,带宽扩容和负载均衡,在生产场景中是一种常用的技术. 生产环境服务器为:DELL 网卡为:光纤 bond需要修改涉及的网卡配置文件 /e ...

  4. js初学总结

    基础 交换变量方式 //利用第三方变量进行交换 var num1 = 10; var num2 = 20; var temp; temp = num1; num1 = num2; num2 = tem ...

  5. Spring Boot 整合 Web 开发

    这一节我们主要学习如何整合 Web 相关技术: Servlet Filter Listener 访问静态资源 文件上传 文件下载 Web三大基本组件分别是:Servlet,Listener,Filte ...

  6. PyCharm中创建项目时,在所创建的python虚拟环境下的pip失效

    在这篇博文里,我简单地叙述了我在使用PyCharm创建一个flask项目时遇到的问题,以及我解决这个问题的过程.其中比较值得注意的点有:①PyCharm创建新项目时的解释器配置②Python虚拟环境的 ...

  7. 阿里云服务器CentOS6.9 tomcat配置域名访问

    之前一直是ip访问项目,今天申请到一个测试域名,想要用设置用域名访问项目. 1.进入阿里云服务器中,修改tomcat中server.xml文件 cd /usr/local/apache-tomcat/ ...

  8. layui table 行按钮事件,启用禁用切换

    {{# ){ }} <a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="forbidden& ...

  9. Jquery Validate 相关参数及常用的自定义验证规则

    一.官网地址:http://bassistance.de/jquery-plugins/jquery-plugin-validation 二.默认校验规则 1 2 3 4 5 6 7 8 9 10 1 ...

  10. Nginx负载均衡配置实例

    面对高并发的问题,企业往往会从两个方面来解决.其一,从硬件上面,提升硬件的配置,增加服务器的性能:另外,就是从软件上,将数据库和WEB服务器分离,使数据库和WEB服务器都能够充分发挥各自的性能,并且二 ...