深入理解Java虚拟机 ch12 Java内存模型和线程 读书笔记

part 5 高效并发

  本部分是本书的最后一个部分,分两个章节。其中,第12章讲Java并发处理的基础——Java内存模型。从物理机模型出发,分析其设计思路和问题,引入Java内存模型以及其针对并发的设计细节。最后,说明了Java中多线程的实现,线程调度以及线程状态转换图。
  第13章则是讲并发的正确性——线程安全和高效性——锁优化。正确性是前提,高效性是追求。本章的内容是建立在Java内存模型基础上的。

ch12 Java内存模型和线程

  Java的并发性大多是通过多线程来实现的。本章从Java并发处理的基本模型——Java内存模型出发,介绍了Java内存模型是怎么在物理机内存模型的基础上,建立起自己平台无关的概念模型的。并详细介绍了JVM是如何通过Java内存模型实现多线程以及多线程之间因为共享和竞争数据而导致问题的解决方案的。最后说明了Java中多线程的实现,线程调度以及Java线程状态转换图。

概述

  Amdahl定律:通过系统中并行化与串行化比重来描述多处理器系统能获得的运算加速能力。
  并发处理的广泛应用使得Amdahl定律代替摩尔定律称为计算机性能发展源动力。
  并发的原因和价值:一方面是因为CPU运算能力强大,更重要的是,其运算速度和其存储与通信子系统速度差距太大,若不并发处理,则CPU大部分时间会出于闲置状态。
  典型应用场合:一个服务器同时对多个客户端提供服务,对应指标每秒事务处理数TPS
  并发与Java:服务端的并发应用程序开发重要而门槛较高。Java语言和虚拟机对此提供了很多高效工具,且有中间件服务器各类框架帮忙处理细节,使得Java在这个领域有着很多很成熟的解决方案。本部分的内容是帮助大家剖开API的内部,了解并发的底层细节。
  内存模型:在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。

一 物理机并发模型

  这里,介绍物理机的并发模型,主要是参考其并发方案设计和对缓存一致性问题的处理方法。
  并发方案设计:考虑到在速度上,CPU>>存储设备;现代计算机系统引入高速缓存作为内存与存储器之间的缓冲:将运算所需的书复制到缓存中,让运算快速进行;等运算结束后再从缓存同步回内存。
  上述的方案很好解决了CPU和内存的速度矛盾,但引入了一个新的问题:缓存一致性
  缓存一致性:在多处理器系统中,每个处理器都有自己的高速缓存,且共享同一主内存。若多个处理器运算同时涉及同一块主内存时,可能导致各自的缓存数据不一致,引发错误,这就是缓存一致性问题。
  缓存一致性问题解决方案:引入缓存一致性协议,让各个处理器在读写操作访问缓存时都遵守协议。该类协议有MSIMESIMOSI等。下图为物理机完整的并发模型概念图:

图片来自

图片来源于参考资料2

  此外,除了高速缓存和其配套解决方案缓存一致性协议外,还有个无法在上图中体现的提高CPU并发能力的方法乱序执行:对输入代码进行乱序执行优化,在计算后将执行结果重组并保证最终结果一致。JVM的JIT中也有类似的指令重排序优化。可以看出,Java内存模型是基于物理机内存模型进行重新设计的。

二 Java内存模型

  Java内存模型设计初衷
  1. 屏蔽各种硬件操作系统的内存访问差异,实现在各种平台下都能达到一致的内存访问效果。
  2. 能妥善解决好并发过程中对原子性可见性有序性的处理。
  设计难点:模型的严谨性——Java的并发内存访问操作不会产生歧义;适用性——有足够的自由空间去利用硬件的特性来获取更好的执行速度。
2.1 主内存和工作内存

Java内存模型

图片来源于参考资料2

  上图即为Java内存模型。Java内存模型规定所有的变量都存储在主内存,每条线程还有自己的工作内存;分别对应物理机并发模型的高速缓存主内存。线程对变量的所有操作都必须在工作内存中进行,不同线程间数据传递必须通过主内存完成。
  Java内存模型的目标:定义程序中各个变量(共享区域变量而不是局部变量)的访问规则,即在虚拟机和内存间存取变量的底层细节。
  所要注意的是,这里的Java内存模型是抽象概念,而第二章里面的内存分区是物理分区,两者之间没有直接关系。对比来看,主内存相当于Java堆中对象实例的数据部分,而工作内存相当于虚拟机栈中的部分区域。
2.2 主内存和工作内存间交互操作和规则
  本小节讲述在上述Java内存模型下,工作内存和主内存之间的交互,涉及到8种操作8项规则。其中,long和double类型非原子性协定规定了其8项操作可以为非原子的,但在实际实现中,可以认为8项基本操作对所有基本数据类型

的操作都具有原子性

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

这里写图片描述

  如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是read和load之间,store和write之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。
  在执行上述八种基本操作时,Java内存模型还规定了必须满足以下8项规则
  1. 不允许read和load、store和write操作之一单独出现
  2. 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  3. 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  4. 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  5. 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
  6. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  7. 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  8. 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
  2.3 volatile变量
  简介:关键字volatile用于修饰变量,是JVM最轻量级的同步机制。JVM为volatile变量提供了两种特性:可见性有序性。而并发操作的安全性需要操作具有原子性可见性有序性这三种特性。所以,volatile的应用情形限于下面两种情形:
  1. 运算结果并不依赖变量的当前值,或者确保只有单一的线程修改变量的值;
  2. 变量不需要与其他的状态变量共同参与不变约束
  遇到其他情形时,需要通过加锁(synchronized关键字或concurrent中的原子类)来保证操作的原子性
  性能说明:volatile变量读操作的性能消耗与普通变量基本没有区别;但写操作可能会慢一些,因为需要插入内存屏蔽指令以保证处理器不发生乱序执行——保证有序性。
  2.4 先行发生原则
  2.2节中的8种操作和8项规则以及2.3节中关于volatile变量的说明,已经能完全确定内存访问操作的并发安全性;本小节讲他们的等效判定原则——先行发生原则
  先行发生原则先行发生是Java内存模型中定义的两项操作之间的偏序关系。满足先行发生原则的情况下,无需添加任何同步措施即可保证线程的安全。
  Java中无需任何同步手段保证就能成立的先行发生规则仅限以下几种:
  程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支,循环等结构。
  管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
  volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。
  线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
  线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到程序已经终止执行。
  线程中断规则(Thread Interrupting Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
  对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
  传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
  2.5 并发三大特性:原子性 可见性 有序性
  该三大特性是针对操作而言的,Java内存模型就是围绕在并发过程中如何处理这三大特性而建立的。下面对三大特性进行分别介绍并说明那些操作实现了这三大特性。
  原子性:表明操作不可再分。基本数据类型的访问读写是具备原子性的,可以用synchronized关键字实现方法或代码块的原子性。
  可见性:可见性指当一个线程修改了共享变量的值,其他线程可以立即得知该修改。JMM是通过在变量修改后将新值同步回主内存,并在变量读取前刷新主内存变量值这种依赖主内存作为传输媒介的方式来实现可见性的。volatilesynchronizedfinal这三个关键字都可以实现可见性。
  有序性:Java程序中天然的有序性可以总结为在本线程内,所有的操作都是有序的,符合先行发生原则;而在一个线程中观察另一个线程,所有的操作都是无序的,涉及指令重排序。这里的有序性是指线程之间操作的有序性,可以通过volatilesynchronized关键词获取。前者通过禁止指令重排序实现;后者通过“一个变量在同一时刻只允许一个线程对其进行lock操作”实现。
  由上述分析可知,synchronized关键字必然可以实现安全并发,而volatile只在原操作满足原子性的情况下使用。但注意不要滥用synchronized,其性能损耗较大。下章的锁优化与其密切相关。

三 Java与线程

  Java中,并发大多是用多线程实现的,本节的内容就是Java线程的实现调度状态转换
  线程是比进程更轻量级的调度执行单位。线程的引入,可以把一个进程的资源分配执行调度分开,各进程可以共享进程资源,又可以独立调度(线程是CPU调度基本单位)。
  Java语言提供了在不同硬件和操作系统平台下对线程操作的统一处理,每个已执行start()且还未结束的java.lang.Thread的实例就代表了一个线程。需要说明的是,Thread类的所有关键方法都是Native的,也就是平台相关的。
3.1 线程的实现
  线程的实现有3种方式:
  1. 使用内核线程实现
  2. 使用用户线程实现(已经弃用)
  3. 使用用户线程加轻量级进程混合实现
  Java线程模型是基于原生线程模型来实现的。就Sun JDK来说,其Windows版和Linux版都是基于上述第一种方法实现的,如下图所示

图片来源见参考

图片来源于参考资料4

  如上图所示,该方式使用内核线程实现线程。内核线程是直接由操作系统内核支持的线程,由操作系统内核来完成线程切换;操作系统内核通过操纵线程调度器Thread Scheduler内核线程KLT进行调度,将线程的任务映射到各个处理器上进行处理,如图的下半部分所示。
  而程序不直接使用内核进程,二是使用内核线程的高级接口——轻量级进程LWP,其与内核进程一一对应。
  优缺点:每个LWP都是一个独立调度单元,阻塞不影响整个程序工作。缺点一是由于基于内核进程实现,需要进行系统调用,需要在用户态和内核态之间切换,代价高;二是LWP与KLT一一对应,因为KLT数量有限,所以LWP数量有限。
3.2 Java线程调度
  线程调度:指为线程分配处理器使用权的过程。主要线程调度方式分两种,协同式线程调度抢占式线程调度
  协同式线程调度:线程的执行时间由线程本身来控制,线程本身完成执行后,通知系统切换至另一线程。优点是实现简单,且没有同步的问题。缺点是执行时间不可控,一个线程出现问题就可能导致系统崩溃。
  抢占式线程调度:每个线程由系统分配执行时间,整体可控。Java则采用本调度方式。
  在Java线程调度即抢占式线程调度中,线程调度是系统完成的,但是可以设置优先级建议系统给某一线程多分配时间。Java语言设置了10个优先级,优先级越高,越有可能在出于Ready状态时被系统选中执行。需要注意的是,Java线程最终是要映射到系统的原生线程来实现的,所以最终调度决定于操作系统。Java语言的优先级和系统的优先级不一定一致。
  3.3 Java线程状态转换
  Java中定义了5种线程状态,状态转换图如下图所示

这里写图片描述

图片来源参考资料5

  说明:
  1. 新建状态(New) : 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
  2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
  3. 运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
  4. 阻塞状态(Blocked) : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
   4.1 等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。
   4.2 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
   4.3 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  5. 死亡状态(Dead) : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

参考资料

  1. 深入理解Java虚拟机 第2版 周志明
  2. Java内存模型
  3. Java内存模型浅析
  4. Java线程模型、线程状态 - 线程(1)
  5. JAVA 线程状态及转化
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值