Java内存模型与线程

TPS:衡量一个服务性能的标准,每秒事务处理的总数,表示一秒内服务端平均能够响应的总数,TPS又和并发能力密切相关。

在聊JMM(Java内存模型)之前,先说一下Java为什么要定义出JMM,那就要从Java内存模型的作用谈起,Java内存模型是用来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。在此之前,C++/C直接使用物理硬件和操作系统的内存模型,因此,会由于平台或者操作系统的不同,有可能导致在一个平台上内存访问正常但是在另一台并发访问内存却会出错。

在JDK1.5之后,Java内存模型已经成熟和完善起来了。

主内存和工作内存

JMM主要的目标是定义程序中各个变量的访问规则,这里的变量包含实例对象,静态变量,数组但不包含方法上的参数和局部变量,因为后者是线程私有的,不存在共享问题。

主内存:JMM规定了所有的变量存在主内存中,他这个名字和操作系统中的主存一样但是意义不一样,此处指的是虚拟机内存的一部分,两者的对比图在下文。
工作内存:线程的工作内存保存了被线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都只能在工作内存中操作,不能直接在主内存操作变量。不同线程之间无法直接访问工作内存中的变量,变量的传递需要靠主内存完成。
线程、主内存和工作内存之间的关系

处理器、高速缓存、主内存之间的交互关系

内存之间的交互

对于工作内存的变量同步到主内存,主内存的变量的拷贝写入到工作内存的实现细节,全部采用以下8中原子操作来完成。

  • lock(锁定):作用于主内存的变量,他把一个变量标识为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,他把处于锁定的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存的变量,将一个变量的值从主内存中传输到线程的工作内存中。以便以后的load操作。
  • load(载入):作用于工作内存的变量,他把read操作从主内存得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,他把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的字节码指令时就会使用到这个变量。
  • assign(赋值):作用于工作内存的变量,他把一个执行引擎接受到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令的时候就执行这个操作。
  • store(存储):作用于工作内存的变量,他把工作内存的变量的值传递给主内存中,一边随后的write操作。
  • write(写入):作用于主内存的变量,他把store操作从工作内存中得到的变量的值放入主内存的变量中。

如果要把变量从主内存复制到工作内存,就要顺序执行read和load操作,如果要把变量从工作内存同步到主内存就要顺序执行 store和write操作。注意顺序执行的含义是,read要在load之前,但是中间可以进行其他的操作例如read a; read b; load b ;load a; 同样store和write也是一样。

以上8中操作必须满足以下8个规则
1、不允许read和load、store和write操作之一单独出现。
2、不允许一个线程丢弃他最近的assign操作,也就是变量在工作内存中改变了之后必须把该变化同步回主内存。
3、不允许一个线程无原因(没有发生任何的assign操作)的将数据从工作内存同步回主内存。
4、不允许新的变量诞生在工作内存换句话说就是store要在load之前,assign要在use之前。
5、一个变量在同一时刻只允许一条线程对其进行lock操作,但是lock可以被同一线程执行多次,多次执行lock后需要执行相同次数的unlock,变量才能被解锁。
6、如果对一个变量执行lock,那将会清除工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或者assign操作初始化变量的值。
7、如果一个变量事先没有被lock,那就不允许对他执行unlock,还有不允许unlock一个被其他线程锁定住的变量。
8、在对一个变量执行unlock之前,必须先把这个变量同步回主内存中(执行store和write)

对于volatile变量的特殊规则

Java内存模型对于volatile专门定义了一些特殊的访问规则。在没有volatile之前,一个线程A修改一个变量的值,然后向主内存同步,另一个线程B才能看到这个变量新的值,因此我们可以说这个变量对于线程B可见。
volatile可以保证可见性但是无法保证原子性,所以在并发条件下被volatile修饰的变量也是线程不安全的。

由于volatile变量只能保证可见性,在不符合以下两个场景中,我们仍要通过加锁来保证原子性。

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  • 变量不需要与其他的状态变量共同参与不变约束。

对于volatile变量的第二个语义是进制指令重排优化。普通变量仅仅会保证执行代码结果的地方都会得到正确的结果,但是不能保证会在执行方法的过程中会按具体操作的执行顺序和你写的代码的顺序一致。在硬件方面上讲,CPU允许多条指令不按程序规定的顺序分发给各个电路单元处理。但是并不是任意的顺序执行,是在保证最后能够正确的处理指令之间的依赖情况,也就是最后结果是正确的前提下。

那volatile是如何禁止指令重排的呢?
被volatile修饰的变量赋值后多执行了lock 地址 操作,这个操作相当于设置了一个内存屏障(内存屏障的作用就是不能把后面的指令重排序到内存屏障之前的位置),当只有一个CPU访问内存的时候,不需要内存屏障,但是如果有两个或更多的CPU访问同一块内存,且其中一个在观测另一个CPU访问,这时候就需要内存屏障保证一致性。它使得CPU中的缓存写入到内存,在这个过程中会让其他CPU或者内核无效化他们自己内存中的被volatile修饰的变量。

对于long和double变量的特殊规则

对于64位的数据类型,在模型中特别定义了 一条相对宽松的规定:允许虚拟机在没有被volatile修饰的long / double类型的变量可以不保证实现原子性(可以划分为对其进行两次32的数据操作),这就是非原子协定。

因为Java内存模型虽然允许虚拟机不把long和double变量的读写操作规定成原子性的,但是虚拟机可以选择把这些操作实现成原子性的操作,虚拟机有这个选择的权利,所以在实际开发中,各个平台的商用虚拟机几乎都选择把64位数据的读写操作作为原子性操作来对待,所以一般不需要把long和double变量专门声明为volatile。

原子性、可见性、有序性

其实Java内存模型具体围绕的就是如何处理这三个特性。
原子性:由Java内存模型保证原子性的操作包括read、 load、 assign、 use、 store、 write。我们大致可以认为这些操作对基本的数据类型都是具备原子性的。如果需要一个大范围的原子操作,Java内存模型提供了lock和unlock操作满足这种需求,还提供了更高层次的直接字节码指令monitorenter和monitorexit来隐式的使用这两个操作,这两个字节码指令反应到Java代码中就是同步块—synchronized关键字。

可见性:可见性是指当一个线程修改了共享变量的值,其他线程能够感知这个修改。JMM是通过在变量修改后将新值同步回主存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式实现可见性的,无论是普通变量还是volatile变量都是如此,但volatile修饰的变量保证了新值能够立即同步到主内存,以及每次使用前从主内存刷新。因此,可以说volatile保证了多线程操作时变量的可见性,而普通变量不能保证这一点。除volatile外还有synchronized和final也可保证内存的可见性。

有序性:Java提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身包含了禁止指令重排的语义,而synchronized是:一个变量在同一时刻只允许一条线程对其进行lock操作。这条规则规定了持有同一把锁的两个同步块只能串行执行。天然的有序性在Java中规定的是:如果在本线程观察的话,所有的操作都是有序的,如果在另外的线程观察另一个线程,所有的操作都是无序的。

先行先发生原则(happen-before原则)

先行先发生是指:
Java内存模型中定义的两项操作之间的偏序关系,如果说A先行于B,其实就是说在发生B操作之前,操作A产生的影响能被操作B观察到,至于这个影响可以是修改内存中的共享变量也可以是发送消息、调用某个方法等。

happen-before要求前一个操作的执行结果对后一个操作可见,并且前一个操作按照顺序排在第二个操作之前。

happen-before规则:
  1. 程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作要先行发生于书写在后面的操作。准确的说,应该是控制流顺序而不是程序代码的顺序。
  2. 管程锁定规则:一个unlock操作要先行发生于lock。这里需要强调的是通一把锁。
  3. volatile变量规则:对一个volatile变量的写操作线性发生于后面对这个变量的读操作,后面是指时间的先后顺序。
  4. 线程启动规则:Thread对象的start方法先行发生于此线程的每个动作。
  5. 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,比如线程A中执行ThreadB.join();name线程B中的任意操作先行于A从ThreadB.join()操作成功返回。
  6. 线程中断规则:对线程Thread.interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  7. 对象终结规则:一个对象的初始化完成要先行于他的finalize()方法的开始。
  8. 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于C。

时间先后顺序与happen-before原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切以happen-before原则为准。

参考 :《深入理解Java虚拟机 》周志明

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

  1. java内存模型与线程(转) good

    java内存模型与线程 参考 http://baike.baidu.com/view/8657411.htm http://developer.51cto.com/art/201309/410971_ ...

  2. Java并发程序设计(三) Java内存模型和线程安全

    Java内存模型和线程安全 一 .原子性 原子性是指一个操作是不可中断的.即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其它线程干扰. 思考:i++是原子操作吗?  二.有序性 Java代 ...

  3. 深入理解java虚拟机-第12章Java内存模型与线程

    第12章 Java内存模型与线程 Java内存模型  主内存与工作内存: java内存模型规定了所有的变量都在主内存中,每条线程还有自己的工作内存. 工作内存中保存了该线程使用的主内存副本拷贝,线程对 ...

  4. jvm(12)-java内存模型与线程

    [0]README 0.1)本文部分文字描述转自“深入理解jvm”,旨在学习“java内存模型与线程” 的基础知识:   [1]概述 1)并发处理的广泛应用是使得 Amdahl 定律代替摩尔定律称为计 ...

  5. (Java多线程系列七)Java内存模型和线程的三大特性

    Java内存模型和线程的三大特性 多线程有三大特性:原子性.可见性.有序性 1.Java内存模型 Java内存模型(Java Memory Model ,JMM),决定一个线程对共享变量的写入时,能对 ...

  6. 深入理解Java虚拟机(第三版)-13.Java内存模型与线程

    13.Java内存模型与线程 1.Java内存模型 Java 内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到主内存和从内存中取出变量值的底层细节 该变量指的是 实例字 ...

  7. 一夜搞懂 | Java 内存模型与线程

    前言 本文已经收录到我的 Github 个人博客,欢迎大佬们光临寒舍: 我的 GIthub 博客 学习导图 一.为什么要学习内存模型与线程? 并发处理的广泛应用是 Amdah1 定律代替摩尔定律成为计 ...

  8. 《深入了解java虚拟机》高效并发读书笔记——Java内存模型,线程,线程安全 与锁优化

    <深入了解java虚拟机>高效并发读书笔记--Java内存模型,线程,线程安全 与锁优化 本文主要参考<深入了解java虚拟机>高效并发章节 关于锁升级,偏向锁,轻量级锁参考& ...

  9. java内存模型和线程

    概述 多任务的处理在现在的计算机中可以说是"标配"了,在许多的情况下,让计算机同时做几件事情,不仅是因为计算机的运算能力的强大,还有一个重要的原因是:cpu的运算速度和计算机的存储 ...

随机推荐

  1. CSP-S2020 DP专项训练

    前言 \(\text{CPS-S2020}\) 已然临近,而 \(\text{DP}\) 作为联赛中的常考内容,是必不可少的复习要点,现根据教练和个人刷题,整理部分好题如下(其实基本上是直接搬--). ...

  2. SpringBoot快速入门(实战篇一)

    SpringBoot快速入门(一) 一SpringBoot简介 1.spring开发经历的阶段 Spring 诞生时是 Java 企业版(Java Enterprise Edition,JEE,也称 ...

  3. 学好Spark/Kafka必须要掌握的Scala技术点(二)类、单例/伴生对象、继承和trait,模式匹配、样例类(case class)

    3. 类.对象.继承和trait 3.1 类 3.1.1 类的定义 Scala中,可以在类中定义类.以在函数中定义函数.可以在类中定义object:可以在函数中定义类,类成员的缺省访问级别是:publ ...

  4. Taro 3.1 beta 发布: 开放式架构新增 4 端支持

    作者:凹凸曼-JJ 自 7 月初我们正式发布了 Taro 3,至今半年时间已然略去.期间我们不断地修复着问题,同时也在构想着下一个 minor 版本. 面对小程序平台越来越多的大环境,Taro 是选择 ...

  5. 深入解析 C# 的 String.Create 的方法

    作者:Casey McQuillan 译者:精致码农 原文:http://dwz.win/YVW 说明:原文比较长,翻译时精简了很多内容,对于不重要的细枝末节只用了一句话概括,但不并影响阅读. 你还记 ...

  6. web服务器专题:tomcat(二)模块组件与server.xml 配置文件

    web服务器专题:tomcat(二)模块组件与server.xml 配置文件 回顾: Web服务器专题:tomcat(一) 基础模块 一个Server.xml的实例 <?xml version= ...

  7. css 02-CSS属性:背景属性

    02-CSS属性:背景属性 #background 的常见背景属性 css2.1 中,常见的背景属性有以下几种:(经常用到,要记住) background-color:#ff99ff; 设置元素的背景 ...

  8. qs的工具方法讲解

    简单来说,qs 是一个增加了一些安全性的查询字符串解析和序列化字符串的库. 今天在学习同事的代码, 在学习过程中遇到了这样一句代码 研究了很久,只了解了个大概,后面慢慢的用熟练只会,想着做个总结,温习 ...

  9. 【electron+vue3+ts实战便笺exe】一、搭建框架配置

    不要让自己的上限成为你的底线 前言 诈尸更新系列,为了跟上脚步,尝试了vue3,在学习vue3的时候顺便学习一手electron和ts,本教程将分别发布,源码会在最后的文章发布.因为还在开发中,目前也 ...

  10. Reset 对象属性

    Input Reset 对象 在 HTML 表单中 标签每出现一次,一个 Reset 对象就会被创建. 当重置按钮被点击,包含它的表单中所有输入元素的值都重置为它们的默认值.默认值由 HTML val ...