java并发编程——内存模型

1. 并发编程基础概念

并发——在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行——源自百度百科

在并发编程中,我们需要处理两个关键问题:线程之间如何通信线程之间如何同步,后续篇章将围绕这两个问题进行介绍。

  • 线程通信:是指线程之间以何种机制来交换信息,在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递
  • 线程同步:是指程序用于控制不同线程之间操作发生相对顺序的机制。在Java中,可以通过volatile,synchronized, 锁等方式实现同步。

本文主要介绍java的通信机制,刚介绍常见通信机制主要包括以下两种方式:

  1. 共享内存:线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。
  2. 消息传递:线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。

Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。在java中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享。

Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。

2. JMM内存模型

JMM(Java Memory Model)是JVM规范中定义的一种Java内存模型,它的目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台上到能达到一致的内存访问效果。
Java内存模型的主要定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。首先简单说明几个常用名称定义:

  • 变量:这里指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。
  • 主内存:在java中,实例域、静态域和数组元素是线程之间共享的数据,它们存储在主内存中。
  • 工作内存:每条线程都有自己的工作内存,线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。
线程、主内存和工作内存之间交互关系

线程、主内存和工作内存的交互关系如上图所示,和CPU-缓存-内存很类似。
不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成
最后注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题

3. 内存间交互操作

关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:

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

所以变量读写包含以下几个步骤:

  1. 变量从主内存复制到工作内存——顺序执行read和load操作
  2. 变量从工作内存同步到主内存——顺序执行store和write操作

注意,Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是read和load之间,store和write之间是可以插入其他指令的。
除了定义以上8中原子操作,Java内存模型还规定了上述8种基本操作在执行时必须满足一定的操作规则,例如如不允许read和load单独出现(即不允许一个变量从主内存中读取但工作内存不接受),不允许store和write单独出现(即不允许从工作内存中发起了回写单主内存不接受),这里不一一列举,详细网上搜索即可。
Java内存模型还定义了volatile型变量的特殊规则(下一节介绍),以上三种规定共同确定了Java中哪些内存访问操作是安全的即:

8种原子操作+操作规则+volatile规定=Java中哪些内存访问操作是安全的

4. volatile型变量规定

当一个变量被定义为volatile后,将具备两种特性:

  • 特性一:保证此变量对所有线程的可见性
  • 特性二:禁止指令重排序优化

4.1 volatile可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
但是,需要注意的是volatile变量只保证可见性,但是java里面的运算并非全部都是原子操作例如++操作,这样同样导致volatile修饰变量java运算不安全。
一般不符合以下两条规则的运算场景中,我们需要通过加锁(synchronized或并发包中的锁)保证变量原子性:

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

常见的volatile修饰变量的场景是用来作为开关控制并发:


volatile开关

4.2 禁止指令重排序

重排序:是指“编译器和处理器”为了提高性能,而在程序执行时会对程序进行的重排序。大致可以分为以下三类:

  • 编译器优化指令重排,不改变单线程语义的情况下,重新安排指令执行的顺序。
  • 指令级并行重排序,该优化主要是为了让程序发挥现代处理器的指令级并行执行能力,前提是这些语句不存在数据依赖。
  • 内存系统重排序,主要发生在处理器读写缓冲区,读写过程看起来是无序的,但最终结果是有序的
    从Java源代码到最终实际执行的指令序列,会经过下面三种重排序:
实际执行指令序列

以上重排序可能会导致多线程中出现内存可见性问题,针对编译器重排序JMM的编译器重排序规则会禁止特定类型的编译器重排序。
而对于后两种重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel称之为memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。

下面我们看下jvm如何实现volatile禁止指令重排序的:

  1. volatile变量写操作,jvm会向处理器发送一条Lock前缀命令,将变量所在的缓存行系会到系统内存。其他处理器通过嗅探总线上传播的数据检测自己的数据是否过期,如果发现过期会置为无效,再次使用时会从系统内存获取
  2. Lock前缀命令禁止该指令与之前和之后的读和写指令重排序。

最后,关于volatile禁止重排序几点使用说明:

  • 不会对volatile读与volatile读后面的任意内存操作重排序
  • 不会对volatile写与volatile写之前的任意内存操作重排序
  • CAS同时具有volatile读和写内存的语义,java的CAS使用现代处理器提供的高效级别的原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是多处理器中实现同步的关键。

5. JMM内存模型总结

总的来说JMM内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性三个特征来建立的。下面就三个特征分别说明:

5.1 原子性

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
java内存模型的read、load、assign、use、store和write六个操作直接保证原子性,我们可以任务基本数据类型访问读写是具有原子性(特殊说明long double64位操作根据jvm实现有关)。
如果场景中需要大范围的原子性保证,java内存模型提供了lock和unlock操作来满足,对应到java代码关键字即是——synchronized。

5.2 可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
除了上面介绍的volatile外,java还提供了两个关键字实现可见性,synchronized和final。

  • final的可见性:是指被final修饰的字段在构造器中一旦完成,那么在其他线程就可以看见final字段值
  • synchronized可见性:是指对一个变量执行unlock操作之前,必须先把次变量同步会主内存这条操作规则限制

5.3 有序性

有序性:即程序执行的顺序按照代码的先后顺序执行。
java中天然有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另外一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句表示“指令重排”和“工作内存与主内存同步延迟”现象。
java提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,这里synchronized则是有“同一时刻只允许一条线程对其进行lock操作”这条操作规定获取的,这个规则决定了同一个锁的两个同步块只能串行进入。

最后,可以发现synchronized关键字可以同时解决上述三个问题,当然这个需要付出代价就是性能问题。

参考文档

《深入理解java虚拟机》——周志明
http://www.cnblogs.com/dolphin0520/p/3920373.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,869评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,716评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,223评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,047评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,089评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,839评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,516评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,410评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,920评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,052评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,179评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,868评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,522评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,070评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,186评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,487评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,162评论 2 356

推荐阅读更多精彩内容

  • 幼来无助伶仃苦, 梦魇年年。 梦魇年年, 强作人前展笑颜。 二十双喜为人母, 病痛绵绵。 病痛绵绵, 笑里朦胧泪水咸。
    清勇卢追阅读 306评论 0 0
  • 师弟问师兄如何才能长生不老,师兄缓缓道:“忘情、无我,浮游沧海之间,百无牵挂,能与天地同寿。”师弟若有所思地去了。...
    洞庭府君阅读 321评论 0 4
  • 2016年是抑郁症困扰的一年,经历了连续痛苦的失眠,经常被恐惧惊醒。抑郁症影响了我的各个方面,几乎丧失了基本的社交...
    871263354579阅读 136评论 0 0
  • 所谓“成人的世界,智商在一个层次上,才能在一起玩”。相信看过《欢乐颂》的人都对赵医生嫌弃曲筱绡无知的事情印象深刻吧...
    秦楚zoro阅读 19,867评论 2 13