@

1. 并发编程的两个问题

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

通信指的是线程之间是以何种机制来交换信息, 在命令式编程中, 线程之间的通信机制有两种:共享内存和消息传递。在共享内存的模型中, 线程之间共享程序的公共状态, 通过读写内存中的公共状态进行隐式通信。在消息传递的并发模型中, 线程之间没有公共状态, 线程之间必须通过发送消息显示的进行通信。

同步指的是程序中用于控制不同线程之间操作发生相对顺序的机制。在共享内存的并发模型里, 同步是显示进行的。 程序员必须显示的指定某个方法或某段代码需要在线程之间互斥。

Java 采用的是共享内存模型, Java线程之间的通信总是隐式的进行, 整个通信过程对程序员完全透明。

2 CPU 缓存模型

2.1 CPU 和 主存

在计算机中, 所有的计算操作都是由 CPU 的寄存器来完成的。 CPU 指令的执行过程需要涉及数据的读取和写入操作。 CPU 通常能访问的都是计算机的主内存(通常是 RAM)。

随着制造工艺等的飞速发展, CPU 不断的发展。 但主存的发展却没有多大的突破, 因此, 差距就越来越大。

因此, 一种新类型的更快的内存-缓存,就出现了(速度越快越贵),用来弥补两者之间的差距。

2.2 CPU Cache

目前, CPU缓存模型如下所示

越靠近CPU, 速度越快。 其速度差异如下

CPU Cache 由多个 CPU Line 构成, CPU Line 被认为是最小的缓存单位。

2.3 CPU如何通过 Cache 与 主内存交互

既然有了 CPU Cache, CPU 就不直接跟内存进行交互了。 在程序运行的过程中, 会将运算所需要的数据从主内存复制到 CPU Cache 中, 这样就可以直接对 CPU Cache 进行读取和写入, 当运算结束之后, 在将结果刷新到主内存中。

通过以上的方式, CPU的吞吐能力得到极大的提高。有了 CPU Cache 之后, 整体的 CPU 和 主内存的交换架构大致如下

在该架构中, 每个CPU的 CPU Cache 是自己本地的, 别的CPU无法访问。

2.4 CPU 缓存一致性问题

就如同我们在自己的程序中使用缓存时一样, CPU 引入了缓存, 提高了访问速度, 但也带来了缓存一致性的问题。

举例

对于 i++ 这个操作, 需要以下几个步骤

  1. 读取主内存值 i 到 CPU Cache 中
  2. 对 i 进行自增操作
  3. 将结果写回 CPU Cache 中
  4. 将数据刷新到缓存中

在单线程的情况下, 该操作是没有任何问题的。 但是在多线程的情况下, 变量 i 会在多个线程的本地内存中都存在副本, 如果两个线程都执行以上操作, 读取到的值刚开始都为 0, 那么在进行两次自增操作之后, 主存中的值仍然为 1。 这就是缓存一致性问题。

为了解决该问题, 聪明的前人发明了两种方法

  1. 通过总线加锁的方式
  2. 通过缓存一致性协议

总线加锁效率太低, 现在都使用的是缓存一致性协议。

最出名的就是传说中的 MESI(Modify, Exclusive, Shared, Invalid) 协议。

  • Modify:当前CPU cache拥有最新数据(最新的cache line),其他CPU拥有失效数据(cache line的状态是invalid),虽然当前CPU中的数据和主存是不一致的,但是以当前CPU的数据为准;
  • Exclusive:只有当前CPU中有数据,其他CPU中没有改数据,当前CPU的数据和主存中的数据是一致的;
  • Shared:当前CPU和其他CPU中都有共同数据,并且和主存中的数据一致;
  • Invalid:当前CPU中的数据失效,数据应该从主存中获取,其他CPU中可能有数据也可能无数据,当前CPU中的数据和主存被认为是不一致的;

MESI 协议为每个 CPU Line 提供状态, 并根据不同状态的操作做出不同的响应。

在 MESI 协议中, 有如下操作

  • Local Read(LR):读本地cache中的数据
  • Local Write(LW):将数据写到本地cache
  • Remote Read(RR):其他核心发生read
  • Remote Write(RW):其他核心发生write

3 Java内存模型(JMM)

3.1 Java内存模型(JMM)

Java 虚拟机规范提供了一种Java 内存模型来屏蔽掉各种硬件和操作系统的内存访问差异, 以实现让 Java 程序在各种平台下都能达到一致性的内存访问效果。

从架构上看, 跟之前提到的物理硬件内存模型有很大的相似度, 但是差别挺大。

  • 主内存: 所有的变量都存储在主内存中(类似于物理硬件的主内存, 不过该内存只是虚拟机内存的一部分)
  • 工作内存: 工作内存中保存了被该线程用到的变量的主内存副本拷贝(取决于虚拟机的实现, 可能复制的只是对象的引用, 对象的某个字段等), 线程对变量的操作(读写等)都必须在工作内存中运行, 而不能直接读写主内存中的变量

不同的线程之间无法访问对方工作内存中的变量, 线程之间变量的传递必须通过主内存进行

3.2 内存间交互操作

变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存, 由以下8种原子操作来完成。

  1. lock: 作用于主内存变量, 它把一个变量标识为一条线程独占的状态
  2. unlock: 作用于主内存的变量, 它把一个处于加锁的变量释放出来, 释放后的变量才可以被其他线程锁定
  3. read: 作用于主内存变量, 它把一个变量的值从主内存传输到线程的工作内存, 一般随后的 load 操作
  4. load: 作用于工作内存的变量, 它把 read 操作从主内存宏得到的值写入工作内存的变量副本中
  5. use: 作用于工作内存的变量, 把工作内存的变量传递给执行引擎, 每当虚拟机遇到一个需要使用到变量值的字节码指令时就会执行该操作
  6. assign: 作用于工作区内存的变量, 它把执行引擎接收到的值赋值给工作内存的变量, 当虚拟机遇到给一个给变量赋值的指令时就会执行这个操作
  7. store: 作用于工作内存变量, 把工作内存中变量的值传送到主内存中, 以便随后的 write 操作
  8. write: 作用于主内存变量, 它把 store 操作从工作内存中得到的变量值放入主内存变量中

Java模型还对这些操作进行了更加细致的限定, 加上 volatile 的一些特殊规定, 就可以确定 Java 程序中哪些内存访问操作在并发下是安全的。

3.3 重排序

重排序是编译器和处理器为了优化程序性能而对指令序列进行重重排序的一种手段。重排序的目的是在不改变程序执行结果的情况下, 尽可能提高并行度。 有以下几种重排序:

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

从源代码到最终实际执行的指令序列, 经历的3种重排序

1属于编译器重排序, 2和3属于处理器重排序。

3.3.1 数据依赖性

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

名称 代码示例 说明
写后读 a=1;
b=a;
写一个变量之后, 在读这个位置
写后写 a=1;
a=2;
写一个变量之后, 再写一个变量
读后写 a=b;
b=1;
读一个变量之后, 再写这个变量

如果对以上的操作并行重排序, 则会改变程序执行的结果。因此, 编译器和处理器在重排序时, 会遵循数据依赖性, 编译器和处理器不会改变存在数据依赖性的两个操作的执行顺序。

此处说的仅仅是单线程的数据依赖性, 多线程的不考虑。

3.3.2 as-if-serial

即不管程序怎么重排序, (单线程)程序的执行结果不能被改变。 编译器、runtime和处理器必须遵循 as-if-serial 语义。

double pi=3.14;         // A
double r=1.0; // B
double area = pi*r*r; // C

在此代码中, A和B都跟C存在数据依赖性, 但是 A 和 B 之间没有依赖性。 因此, C 不能被排到 A或B 之前。 但对 A 和 B, 这两者可以随意排序。

3.3.3 程序顺序规则

在以上圆形面积的计算中, 有如下三个 happens-before 关系

  1. A happens-before B
  2. B happens-before C
  3. A happens-before C

其中第三条是根据前面两条传递性推倒出来的。

A happens-before B 并不是要求 A 一定要在 B 之前执行, 而是要求A的执行结果对B可见。 但这里的A的执行结果不需要对B可见, 在这种情况下, JMM 会认为这种重排序是合法的, JMM 允许此类重排序。

3.4 happens-before原则

happens-before 是用来阐述操作之间的可见性。 即在JMM中, 如果一个操作执行的结果需要对另一个操作可见, 则这两个操作之间必须存在 happens-before 关系。

happens-before 规则

  1. 程序顺序规则(单线程): 一个线程中的每个操作, happens-before 于该线程中的后续操作。
  2. 监视器规则: 对一个锁的解锁, happens-before 于对该锁的加锁
  3. volatile规则:对一个 volatile 域的写, happens-before 于随后对这个域的读
  4. 传递性: 如果 A happens-before B, 且 B happens-before C, 则 A happens-before C。
  5. 线程启动规则: 如果线程A执行操作ThreadB.start()(线程B启动), 那么A线程的 Thread.start() 操作 happens-before 于线程B的任意操作。
  6. 线程终止规则: 如果线程 A 执行操作 ThreadB.join() 并成功返回, 那么编程B中的任意操作 happens-before 于线程A从ThreadB.join()操作成功返回。
  7. 程序中断规则: 对线程interrupt()的方法的调用 happens-before 于被中断线程代码检测到中断事件的发生。
  8. 对象终结规则: 一个对象的初始化完成, happens-before 于发生它的 finalize() 方法的开始。

3.4 原子性、可见性和有序性

JMM 是围绕着在并发过程中如何处理原子性、可见性和有序性这个三个特征来建立的。

3.4.1 原子性

Java 中对以上的八种操作是原子性的。 对应起来就是对基本数据类型的读取/赋值操作都是原子性的, 引用类型的读取和赋值也是如此。

举几个例子

赋值操作

a=10

该操作需要使用 assign 操作, 可能需要 store 和 write 操作。 这些过程都是原子操作。

可有通过

  1. synchronized关键字
  2. JUC所提供的显式锁Lock

来实现原子性

3.4.1 可见性

指的是一个线程中修改了共享变量, 其他的线程就能够立即知道这个修改。 JMM 可以通过以下三种方式来保证可见性

  1. volatile关键字
  2. synchronized关键字
  3. JUC所提供的显式锁Lock

3.4.2 有序性

Java 中天然的有序性可以概括总结为一句话:如果本线程内观察, 所有的操作都是有序的; 如果在一个线程内观察另一个线程, 所有的操作都是无序的。 前半句指的是 as-if-serial 语义, 后半句指的是“指令重排”和“线程内存与主内存同步延迟”的线程。

有序性的保证:

  1. volatile: 禁止指令重排
  2. synchronized: 一个变量再同一时刻, 只允许一条线程对其进行 lock 操作。
  3. Lock: 同 synchronized

Java 多线程(六)之Java内存模型的更多相关文章

  1. 【java多线程系列】java内存模型与指令重排序

    在多线程编程中,需要处理两个最核心的问题,线程之间如何通信及线程之间如何同步,线程之间通信指的是线程之间通过何种机制交换信息,同步指的是如何控制不同线程之间操作发生的相对顺序.很多读者可能会说这还不简 ...

  2. java多线程的基础-java内存模型(JMM)

    在并发编程中,需要处理两个关键问题:线程之间如何通信,以及线程之间如何同步.通信是指线程之间如何交换信息,在命令式编程中,线程之间的通信机制有两种:内存共享和消息传递.      同步是指程序中用于控 ...

  3. Java多线程学习笔记之三内存屏障与Java内存模型

    基本内存屏障 处理器支持那种内存重排序,就会提供能够禁止相应内存重排序的的指令,这些指令就被成为基本内存屏障:StroeLoad屏障.StroeLoad屏障.LoadLoad屏障.LoadStore屏 ...

  4. java多线程解读二(内存篇)

    线程的内存结构图 一.主内存与工作内存 1.Java内存模型的主要目标是定义程序中各个变量的访问规则.此处的变量与Java编程时所说的变量不一样,指包括了实例字段.静态字段和构成数组对象的元素,但是不 ...

  5. 深入理解java虚拟机(6)---内存模型与线程 & Volatile

    其实关于线程的使用,之前已经写过博客讲解过这部分的内容: http://www.cnblogs.com/deman/category/621531.html JVM里面关于多线程的部分,主要是多线程是 ...

  6. 【java多线程系列】java中的volatile的内存语义

    在java的多线程编程中,synchronized和volatile都扮演着重要的 角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的可见性,可见性指的是当一 ...

  7. Java原子性、可见性、内存模型

    原子性: 原子性就是指该操作是不可再分的.不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作.简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性.比如 a = ...

  8. 深入理解Java虚拟机读书笔记8----Java内存模型与线程

    八 Java内存模型与线程   1 Java内存模型     ---主要目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节.     ---此处的变量和J ...

  9. Java多线程-并发协作(生产者消费者模型)

    对于多线程程序来说,不管任何编程语言,生产者和消费者模型都是最经典的.就像学习每一门编程语言一样,Hello World!都是最经典的例子. 实际上,准确说应该是“生产者-消费者-仓储”模型,离开了仓 ...

  10. Java虚拟机解析篇之---内存模型

    今天闲来无事来,看一下Java中的内存模型和垃圾回收机制的原理.关于这个方面的知识,网上已经有非常多现成的资料能够供我们參考,可是知识还是比較杂的,在这部分知识点中有一本书不得不推荐:<深入理解 ...

随机推荐

  1. 使用 new XMLHttpRequest() 制作下载文件进度条

    mui 进度控件使用方法: 检查当前容器(container控件)自身是否包含.mui-progressbar类: 当前容器包含.mui-progressbar类,则以当前容器为目标控件,直接显示进度 ...

  2. mysql练习----SUM and COUNT/zh图(二)

    世界国家概况 GROUP BY 和 HAVING 通过包括一个GROUP BY子句功能, SUM并将COUNT 其应用于共享值的项目组.当你指定 GROUP BY continent 结果是每个不同的 ...

  3. python len()函数的用法

    函数:len() 返回字符串.列表.字典.元组等长度. 语法:len(str) str:要计算的字符串.列表.字典.元组等 返回值:字符串.列表.字典.元组等元素的长度. Test: 1:计算字符串的 ...

  4. jQuery中 对标签元素操作(1)

    一:创建元素节点(添加) 创建元素节点并且把节点作为元素的子节点添加到DOM树上    append(): 在元素下添加元素    用法:$("id").append(" ...

  5. 登录Windows界面前执行自定义脚本

    通常情况下,进入Windows界面之前都有一个登录过程,如何在登录前让系统执行脚本呢?下面介绍一种方法. 1.打开组策略,在Run(运行)中输入GREDIT.MSC,点击确认. 2.依次点击Compu ...

  6. June 16. 2018, Week 24th. Saturday

    Success is the ability to go from one failure to another with no loss of enthusiasm. 成功,就是即使经历过一个又一个 ...

  7. JavaScript函数式编程

        一段糟糕透顶的海鸥seagulls程序   鸟群合并conjoin则变成了一个更大的鸟群,繁殖breed则增加了鸟群的数量,增加的数量就是它们繁殖出来的海鸥的数量 //Flock 群 var ...

  8. ES5-ES6-ES7_解构赋值

    解构赋值的概念 ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring) 传统对变量赋值的方式,以前,为变量赋值,只能直接指定值. var a = 1; ...

  9. Java(使用JNA)调用DLL库与C#调用DLL库的对比

    前言:在项目中经常使用DLL库对硬件进行操作,在发卡过程中使用频率尤为多,今天就Java与C#中调用DLL库的使用区别做一个介绍,本文着重具体的代码编写,具体过程看以下代码. 前提条件: 笔者已经封装 ...

  10. centos 7.2 64位安装redis

    1.下载redis 可以在新建  /usr/local/redis 文件夹 $ wget http://download.redis.io/releases/redis-4.0.9.tar.gz $ ...