《JRockit权威指南:深入理解JVM》试读:4.1 基本概念

本章将详细介绍Java与JVM中的线程和同步。线程用于在单进程中实现多任务的并行执行,锁用于控制对临界区(critical section)的同步访问,这些是实现并行化任务执行的基本要素。 在本章中,你将学到如下内容。  Java中线程与同步的基本概念,如何利用Java API实现同步,一些关键字,包括wait、notify、常被误用的volatile以及java.util.concurrent包。  Java内存模型及其存在的必要性。理解Java内存模型,是编写多线程应用程序的关键。  JVM如何高效实现多线程与同步操作。本章将讲解几种不同的线程模型。  在自适应运行时反馈的基础上,JVM如何使用不同类型的锁、锁策略和代码优化策略优化线程和同步操作。  避开并行编程陷阱和伪优化,以及java.lang.Thread类中已废弃的方法和双检查锁缺陷。  如何调整JRockit中线程与同步的运行行为,以及如何分析锁的运行情况。 4.1 基本概念 Java从诞生之初起就在语言级别支持并行编程,例如内建的java.lang.Thread类是对操作系统中线程的抽象,还有用于同步操作的关键字synchronized,以及wait和notify方法。这在当时是独一无二的,至少在学术界以外是这样的。那时,商界编程语言还依赖于操作系统提供的支持库来使用线程,而Java提供了与平台无关的方式来操作线程,与以往相比,这可谓是一大突破。 就同步操作来说,Java做得非常好,这不仅是因为它显式地支持线程、锁和信号量,而且其内建机制使每个对象都可以作为监视器使用。Java 1.5引入了java.util.concurrent包,其中包含了很多设计精巧、可用于并行编程的数据结构。 【监视器用于对需要同步的资源加锁,每次只能有一个线程持有该监视器,因此可以实现对资源的排他性访问。】 这种设计的优势很明显:同步操作无须涉及第三方库的调用,而且可以使锁具有完整的语义,在编程的时候便于使用。 硬要说缺点的话,就是使用起来太容易了,有些人可能会滥用同步操作,结果导致应用程序的整体性能大幅下降。 当然,在具体实现上面还有一些可优化的地方。由于每个对象都可以作为监视器使用,每个对象都持有与同步操作相关的信息,例如当前对象是否作为锁使用,以及锁的具体实现等。一般情况下,为了便于快速访问,这些信息被保存在每个对象的对象头的锁字(lock word)中。与自动内存管理类似,性能优化的问题在多线程操作中同样存在。因此,必须能够快速获取目标对象的垃圾回收信息,例如其垃圾回收状态。第3章介绍引用跟踪垃圾回收时提到的标记位就记录了这类信息。JRockit使用锁字中的一些位来存储垃圾回收状态信息,虽然其中包含了垃圾回收信息,但是本书还是称之为锁字。 如果将对象头中存储的信息过度编码的话,那么在使用的时候,就不得不花额外的力气去解码;如果不经编码直接存储,又会消耗大量的内存。因此,在存储每个对象的锁信息和垃圾回收信息时,需要仔细权衡。 对象头还包含了指向类型信息的指针,在JRockit中,这称为类块(class block)。 下图是JRockit中Java对象在不同的CPU平台上的内存布局。为了节省内存,并加速解引用操作,对象头中所有字的长度是32位。类块是一个32位的指针,指向另一个外部结构,该结构包含了当前对象的类型信息和虚分派表(virtual dispatch table)等信息。

就目前来看,在绝大部分JVM(包括JRockit)中,对象头是使用两个32位长的字来表示的。在JRockit中,偏移为0的对象指针指向当前对象的类型信息,接下来是4字节的锁字。在SPARC平台上,对象头的布局刚好反过来,因为在使用原子指令操作指针时,如果没有偏移的话,效率会更高。与锁字不同,类块并不为原子操作所使用,因此在SPARC平台上,类块被放在锁字后面。 【原子操作(atomic operation)是指全部执行或全部不执行的本地指令。当原子指令全部执行时,其操作结果需要对所有潜在访问者可见。】 原子操作用于读写锁字,具有排他性,这是实现JVM中同步块的基础。 【研究表明,在目前的基础上,再压缩对象头(例如将之压缩为单个32位的字)已经没什么意义了。即使这样做可以节省出更多的内存,但在使用的时候需要额外的解码操作,得不偿失。】 4.1.1 难以调试 对于大多数平台和编程语言来说,并发问题可能会以多种形式表现出来,例如死锁(deadlock)、活锁(livelock)和系统崩溃等,它们彼此之间没什么共性。这个问题可谓是老生常谈了。由于并发问题往往与时序相关,即便是连接了调试器也可能无法重现问题。附加的调试器会增加额外的开销,导致时序变更。 【死锁是指两个线程都在等待对方释放自己所需的资源,结果导致两个线程都进入休眠状态。很明显,它们再也醒不过来了。活锁的概念与死锁类似,区别在于线程在竞争时会采取主动操作,但无法获取锁。这就像两个人面对面前进,在一个很窄的走廊相遇,为了能继续前进,他们都向侧面移动,但由于移动的方向相反导致还是无法前进。】 由于存在上述问题,调试并行系统是一件非常困难的任务,而一些可视化工具和调试器可以帮助解开线程间的锁依赖,这对于并发程序调试来说,已经是巨大的帮助了。 像其他主流JVM一样,JRockit可以在控制台里输出当前应用程序中所有线程的调用栈,并打印锁的持有者信息。对于简单的死锁问题来说,这些信息已经足够用来解决问题,可以确定哪些互相依赖的线程在等待同一资源。本章会对此做举例说明。 JRockit Mission Control套件提供了可视化组件来显示线程的锁信息。 4.1.2 难以优化 除了难以调试外,在并行编程中使用锁还会极大地降低应用程序的整体性能。每个锁都是一个性能瓶颈,它保证了对临界区的排他性访问,却使得没有获取锁的线程不得不排队等待。如果锁放置错位,或者控制的临界区过大,就会导致应用程序的性能大幅下降。 不幸的是,很多商业软件的延迟问题就是一两个锁使用不当导致的,我们调试第三方应用程序时曾多次遇到这种情况,开发人员自己也没有意识到对锁的错误使用。幸运的是,若使用不当的锁不多,而且可以识别出来的话,延迟问题还比较容易解决。再次强调,使用JRockit Mission Control可以很容易找到竞争最激烈的锁,以便排查问题。 【锁竞争激烈是指多个线程花费大量时间试图获取某个锁。 】 延迟分析 JRockit Mission Control套件附带了延迟分析组件,可以记录Java应用程序的运行信息,提供可视化的延迟分析数据。在优化大量使用同步操作的应用程序时,延迟分析可以给程序员带来很大的帮助。以往的分析工具只是展示了应用程序将时间花在了什么地方,而延迟分析仪则可以给出应用程序没有花时间在什么地方。当应用程序线程没有执行Java代码时,就将之记录到线程图中,这样就可以判断出,线程是在等待I/O,还是在等待获取锁。 【第8章和第9章将详细介绍如何使用JRockit Mission Control进行延迟分析。】 下图是JRockit运行时分析仪中延迟分析标签页的内容,其数据来自于一个正在运行的服务器端应用程序,用于做离线分析。图中的横条标明了应用程序线程都将时间花在了何处,每当出现新类型的延迟时,就使用一种不同的颜色来标明。时间轴的顺序是最左到右。在图中,线程绝大部分都是红色的,表明线程 “阻塞在Java中”,这很糟,它表明应用程序将时间都花在了等待Java锁上,例如获取同步块的锁。更准确地说,所有非绿色的横条都表示“没有在执行Java代码”,即正在等待I/O、网络通信等资源的本地线程。

回忆一下第3章对延迟的讨论,如果JVM将时间都花在垃圾回收上,就无法执行Java代码了。类似地,如果CPU资源都浪费在等待I/O或Java锁上,就会出现延迟。这也是大部分性能问题产生的根本原因。 【JRockit Flight Recorder套件可以帮助定位Java程序中造成延迟的问题点。在上面的例子中,延迟的根源在于日志模块中存在对锁的不当使用。】

>JRockit权威指南:深入理解JVM

JRockit权威指南:深入理解JVM
作者: [瑞士] 马库斯·希尔特, [瑞典]马库斯·拉杰格伦
isbn: 7115500452
书名: JRockit权威指南:深入理解JVM
页数: 336
译者: 曹旭东
定价: 99.00元
出版社: 人民邮电出版社
出版年: 2019-1
装帧: 平装