内存模型

在计算机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. Java中关于泛型集合类存储的总结

    集合类存储在任何编程语言中都是很重要的内容,只因有这样的存储数据结构才让我们可以在内存中轻易的操作数据,那么在Java中这些存储类集合结构都有哪些?内部实现是怎么样?有什么用途呢?下面分享一些我的总结 ...

  2. 前端之JavaScript篇

    一. 简介 javascript是一门动态弱类型的解释性编程语言, 增强页面动画效果,实现页面与用户之间实时动态的交互.  JavaScript有三部分组成: ECMAscript, DOM, BOM ...

  3. linux iconv 转换文件编码

    查看文件编码file -i filename 递归转换(包括子文件夹)find default -type d -exec mkdir -p utf/{} \;find default -type f ...

  4. 实操:Could not autowire No beans of 'FastDFS Client' type found 的解决方法

    前言: 今天接手了同事之前做的一个小项目,里面涉及到了 FastDFS 的使用.但是当我在本地运行项目的时候,却报了 Could not autowire No beans of 'FastDFS C ...

  5. Mysql高手系列 - 第14篇:详解事务

    这是Mysql系列第14篇. 环境:mysql5.7.25,cmd命令中进行演示. 开发过程中,会经常用到数据库事务,所以本章非常重要. 本篇内容 什么是事务,它有什么用? 事务的几个特性 事务常见操 ...

  6. loadrunner12录制手机app

    今天第一次使用LR12录制app,遇到了录制不上的问题,最终解决了,记录一下 我安装的版本是12.02社区版   HP_LoadRunner_12.02_Community_Edition_T7177 ...

  7. 带UI的小初高数学学习软件

    结对编程项目总结   一.项目需求分析与功能总结 (1)用户注册功能 用户提供手机号码,点击注册将收到一个注册码,用户可使用该注册码完成注册. (2)设置密码功能 密码6-10位,必须含大小写字母和数 ...

  8. 《HelloGitHub》第 42 期

    兴趣是最好的老师,HelloGitHub 就是帮你找到兴趣! 简介 分享 GitHub 上有趣.入门级的开源项目. 这是一个面向编程新手.热爱编程.对开源社区感兴趣 人群的月刊,月刊的内容包括:各种编 ...

  9. Tomcat+Nginx+Linux+Mysql部署豆瓣TOP250的项目到腾讯云服务器

    写在前面 因为前面有写过一篇关于豆瓣的top250的电影的可视化展示项目,你可以移步http://blog.csdn.net/liuge36/article/details/78607955了解这个项 ...

  10. Spring 梳理 - 视图解析器 VS 视图(View,ViewResolver)

    View View接口表示一个响应给用户的视图,例如jsp文件,pdf文件,html文件等 该接口只有两个方法定义,分别表明该视图的ContentType和如何被渲染 Spring中提供了丰富的视图支 ...