Java内存模型与线程

概述

  多任务处理在现代计算机操作系统中几乎已是一项必备的功能,多任务运行是压榨手段,就如windows一样,我们使劲的压榨它运行多个任务,俱要high又要耍。并发则是另外一种更具体的应用场景。每秒事物处理数(Transactions per Second,tps)是最重要的指标。开发人员应该了解与运用并发。

硬件的效率与一致性

  除了有软件上的并发,物理计算机也有并发问题。计算机的存储设备与处理器运算速度有几个数量级的差距,现代计算机都不得不加入一层高速缓存来作为内存与处理器之间的缓冲,这样能够提升处理速度。基于高速缓存解决了处理器与内存的速度矛盾,但是也提高了计算机系统复杂度,带来了缓存一致性问题。在多处理器系统中,每个处理器有自己的高速缓存,而它们又共享同一个主内存。如图:

  多个处理器的任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。为了解决这种一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在操作时要根据协议来进行操作,这类协议有MSI、MESI(Illinois Protoclo)、MOSI、Synapse、Firefly以及Dragon Protocal等。

  除了增加高速缓存之外,为了使处理器得到充分利用,处理器可能会对输入代码进行乱序执行(Out-of-Order Execution)优化,处理器之后会对乱序执行的结果重组,保证与顺序执行的结果是一致的,不保证各个语句计算的先后顺序由于输入代码中顺序一致。这样导致的结果是一个计算任务依赖于另外一个计算任务的中间结果,其顺序性不能依靠代码的顺序性来保存。Java中也存在类似的指令重排序(Instruction Reorder)优化。

java内存模型

  Java虚拟机规范中试图定义一直java内存模型来屏蔽各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能够达到一致的内存访问效果。定义内存模型不是一件容易的事情,要足够严谨足够宽松。严谨是为并发内存操作不会产生歧义,宽松是为有足够空间去利用各种硬件的特性。

主内存与工作内存

  Java内存模型规定所有的变量的存储在主内存中,主内存是java虚拟机内存的一部分,每个线程还有自己的工作内存。线程的工作内存保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接操作主内存。不同线程之间相互隔离。线程间变量值的传递需要通过主内存完成。三者关系如下:

  这里的主内存、工作内存与Java内存区域的Java堆、栈、方法区不是同一层次内存划分。

内存间交互操作

  关于主内存与工作内存之间具体的交互协议,java内存模型定义了以下8种操作来完成。

    lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。

    unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

    read(读取):作用于主内存变量,把 一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用

    load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

    use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

    assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

    store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。

    write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

  如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是read和load之间,store和write之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

  ① 不允许read和load、store和write操作之一单独出现

  ② 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。

  ③ 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。

  ④ 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。

  ⑤ 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现

  ⑥ 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值

  ⑦ 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。

  ⑧ 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

对于volatitle型变量的特殊规则

  关键字volatitle是java虚拟机提供的轻量级的同步机制,正确、完整的理解它有难度。了解volatitle关键字对了解多线程操作的其他特征很有意义。volatitle关键字定义的变量有2种特性:

    第一种保证此变量对所有线程的可见性。

    第二种是禁止指令重排序优化。

  volatitle变量只能保证可见性,在不符合以下2条规则的运算场景中,我们仍然需要通过加锁来保证原子性:

    ① 运算结果不依赖变量的当前值,或者能够保证只有单一的线程修改变量的值。

    ② 变量不需要与其他的状态变量共同参与不变约束。

  volatitle修饰的变量关键变化是多执行了一个“lock addl $0x0,(%esp)”操作,这个操作相当于一个内存屏障(Memory Barrier或Memory Fence,重排序不能把后面的指令重排序到内存屏障位置之前)

  为什么选择volatitle(在一定情况下)?

    在读操作的性能消耗与普通变量几乎没什么区别,在写操作慢一些。虚拟机对锁有许多消除与优化。volatitle关键字就介于普通变量与锁之间。

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

  Java内存模型lock、unlock、read、load、assign、user、store、write这8个操作都有原子性,但是java内存模型将没有被volatile修饰的64位的数据的读写操作划分为两次32为的操作来进行,这样的话,多线程并发,就会存在线程可能读取到“半个变量”的值,不过,这种情况非常罕见,目前各平台的商用虚拟机几乎都选择把64位的读写作为原子操作来实现规范的。

原子性、可见性与有序性

  原子性:原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。

  可见性:可见性就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。

  有序性:Java内存模型中的程序天然有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存中主内存同步延迟”现象。

先行发生原则(Happens-Before)

  先行发生原则(Happens-Before)是判断数据是否存在竞争、线程是否安全的主要依据。先行发生是Java内存,模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,那么操作A产生的影响能够被操作B观察到。

  Java内存模型中存在的天然的先行发生关系:

    1. 程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构。

    2. 管程锁定规则:一个unlock操作先行发生于后面(时间上)对同一个锁的lock操作。

    3. volatile变量规则:对一个volatile变量的写操作先行发生于后面(时间上)对这个变量的读操作。

    4. 线程启动规则:Thread的start( )方法先行发生于这个线程的每一个操作。

    5. 线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过Thread.join( )方法结束、Thread.isAlive( )的返回值等手段检测线程的终止。

    6. 线程中断规则:对线程interrupt( )方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupt( )方法检测线程是否中断

    7. 对象终结规则:一个对象的初始化完成先行于发生它的finalize()方法的开始。

    8. 传递性:如果操作A先行于操作B,操作B先行于操作C,那么操作A先行于操作C。

  总结:一个操作“时间上的先发生”不代表这个操作先行发生;一个操作先行发生也不代表这个操作在时间上是先发生的(重排序的出现)。时间上的先后顺序对先行发生没有太大的关系,所以衡量并发安全问题的时候不要受到时间顺序的影响,一切以先行发生原则为准。

《深入理解java虚拟机-高效并发》读书笔记的更多相关文章

  1. csapp读书笔记-并发编程

    这是基础,理解不能有偏差 如果线程/进程的逻辑控制流在时间上重叠,那么就是并发的.我们可以将并发看成是一种os内核用来运行多个应用程序的实例,但是并发不仅在内核,在应用程序中的角色也很重要. 在应用级 ...

  2. CSAPP 读书笔记 - 2.31练习题

    根据等式(2-14) 假如w = 4 数值范围在-8 ~ 7之间 2^w = 16 x = 5, y = 4的情况下面 x + y = 9 >=2 ^(w-1)  属于第一种情况 sum = x ...

  3. CSAPP读书笔记--第八章 异常控制流

    第八章 异常控制流 2017-11-14 概述 控制转移序列叫做控制流.目前为止,我们学过两种改变控制流的方式: 1)跳转和分支: 2)调用和返回. 但是上面的方法只能控制程序本身,发生以下系统状态的 ...

  4. CSAPP 并发编程读书笔记

    CSAPP 并发编程笔记 并发和并行 并发:Concurrency,只要时间上重叠就算并发,可以是单处理器交替处理 并行:Parallel,属于并发的一种特殊情况(真子集),多核/多 CPU 同时处理 ...

  5. 读书笔记汇总 - SQL必知必会(第4版)

    本系列记录并分享学习SQL的过程,主要内容为SQL的基础概念及练习过程. 书目信息 中文名:<SQL必知必会(第4版)> 英文名:<Sams Teach Yourself SQL i ...

  6. 读书笔记--SQL必知必会18--视图

    读书笔记--SQL必知必会18--视图 18.1 视图 视图是虚拟的表,只包含使用时动态检索数据的查询. 也就是说作为视图,它不包含任何列和数据,包含的是一个查询. 18.1.1 为什么使用视图 重用 ...

  7. 《C#本质论》读书笔记(18)多线程处理

    .NET Framework 4.0 看(本质论第3版) .NET Framework 4.5 看(本质论第4版) .NET 4.0为多线程引入了两组新API:TPL(Task Parallel Li ...

  8. C#温故知新:《C#图解教程》读书笔记系列

    一.此书到底何方神圣? 本书是广受赞誉C#图解教程的最新版本.作者在本书中创造了一种全新的可视化叙述方式,以图文并茂的形式.朴实简洁的文字,并辅之以大量表格和代码示例,全面.直观地阐述了C#语言的各种 ...

  9. C#刨根究底:《你必须知道的.NET》读书笔记系列

    一.此书到底何方神圣? <你必须知道的.NET>来自于微软MVP—王涛(网名:AnyTao,博客园大牛之一,其博客地址为:http://anytao.cnblogs.com/)的最新技术心 ...

  10. Web高级征程:《大型网站技术架构》读书笔记系列

    一.此书到底何方神圣? <大型网站技术架构:核心原理与案例分析>通过梳理大型网站技术发展历程,剖析大型网站技术架构模式,深入讲述大型互联网架构设计的核心原理,并通过一组典型网站技术架构设计 ...

随机推荐

  1. eclipse中创建类和方法自动注释

    <?xml version="1.0" encoding="UTF-8"?><templates><template autoin ...

  2. 1.1.3.托管对象上下文(Core Data 应用程序实践指南)

    管理托管对象的生命周期(lifecycle).还有其它功能:faulting.变更追踪(change tracking).验证(validation)等. faulting:只把用到的那一部分数据从持 ...

  3. 论MySQL数据库中两种数据引擎的差别

    InnoDB和MyISAM是在使用MySQL最常用的两个表类型,各有优缺点,视具体应用而定. 基本的差别为: MyISAM类型不支持事务处理等高级处理,而InnoDB类型支持. MyISAM类型的表强 ...

  4. java_web学习(1)理解JavaBean

    JavaBean简介       JavaBean是一种特殊的 Java 类,它遵从一定的设计模式,开发工具和其他组件可以根据这种模式来调用JavaBean. JavaBean可以设计得像Swing组 ...

  5. 字符集 ISO-8859-1(1)

    HTML 4.01 支持 ISO 8859-1 (Latin-1) 字符集. ISO-8859-1 的较低部分(从 1 到 127 之间的代码)是最初的 7 比特 ASCII. ISO-8859-1 ...

  6. Windows与Linux文件系统互访的几种方法

    首先,我们知道基于文件的几种服务:ftp,sftp,这两种服务都是文件传输服务,偏重于网络传输,并不是实时互访.通常,我们需要在远程和本地 同时操作同一个目录,如:在Windows下使用各种强大的ID ...

  7. js架构设计模式——由项目浅谈JS中MVVM模式

    1.    背景 最近项目原因使用了durandal.js和knockout.js,颇有受益.决定写一个比较浅显的总结. 之前一直在用SpringMVC框架写后台,前台是用JSP+JS+标签库,算是很 ...

  8. --@angularJS--指令与控制器之间的交互demo

    1.index.html: <!DOCTYPE HTML><html ng-app="app"><head>    <title>c ...

  9. ViewFlipper的功能与用法

    ViewFlipper组件继承了ViewAnimator,它可调用addView(View v)添加多个组件,一旦向ViewFlipper中添加了多个组件之后,ViewFlipper可使用动画控制多个 ...

  10. thinkphp 配置项总结

    'URL_PATHINFO_DEPR'=>'-',//修改URL的分隔符 'TMPL_L_DELIM'=>'<{', //修改左定界符 'TMPL_R_DELIM'=>'}&g ...