Java并发基石——所谓“阻塞”:Object Monitor和AQS(1)

通过上文的介绍我们知道就算是“阻塞”状态,根据进入阻塞状态的方式不同,阻塞状态也会有细微的差异。这样的差异基本上分成两种大的类型:Object Monitor和Parking。在本文和后续的几篇文章中,我们将对它们进行详细介绍。我们将首先介绍基于Object Monitor原理的悲观锁实现,然后再讨论基于AQS队列同步框架。

1、所谓“阻塞”——Object Monitor和AQS

在本专题开始的时候曾经通过一张图的方式介绍了线程的几个状态和这几个状态的切换方式,如下图所示:

// TODO 这里差一张以前的图

通过最近几篇文章的介绍,我们知道了就算是“阻塞”状态,也是有区别的。通过不同的阻塞机制可以使线程进入不同的“阻塞”状态。如下图所示:

在这里插入图片描述

如上图所示,根据本专题文章的进程,我们将“线程状态”切换这张图进行了“些许调整”。主要调整的是“阻塞”这种状态。我们标识出了不同的方法(或类似的方法)进入的不同阻塞状态,但是由于图片大小原因还不能穷尽所有方法,例如使用ReentrantLock对Condition接口的具体实现中的await方法,也可以使当前线程进入“阻塞”状态(parking)。

从上图中可以看出来,这里的几个主要“阻塞”状态可以归纳为“sleeping”、“on object monitor”、“parking”以及一种“BLOCKED”的阻塞状态。实际上这几种有细节区别的“阻塞”状态,恰恰就是Java中不同的锁机制实现

2、Object Monitor机制

在本专题之前的文章中我们提到,Java中对悲观锁思想的实现就是我们最常使用的synchronized同步块。而Object Monitor机制就是synchronized同步块锁机制升级为“重量级锁”后的工作机制。本节我们首先讲解synchronized和锁升级过程,然后再讲解Object Monitor机制的工作过程。

2.1、synchronized和锁升级过程

2.1.1、和synchronized有关的对象结构

Java对象结构中的对象头描述部分是实现锁机制的关键,实际上在HotSpot JVM 虚拟机的工作中将对象结构分为三大块区域:对象头(Header)、实例数据(Instance Data)和对齐填充区域(可能存在)。如下图所示:

在这里插入图片描述

  • 对齐填充区域(Padding):对齐填充区域并不是必须存在的,它只是起到占位作用——这是因为HotSpot JVM 虚拟机要求被管理的对象的大小都是8字节的整数倍。那么在某些情况下,就需要填充区域对不足的对象区域进行填充(随后的实例中会有)

  • 实例数据:这个区域当然就是描述真实的对象数据。这个区域包括了对象中的所有字段属性信息,它们可能是某个其它对象的地址引用,也可能是基础数据的数据值。

  • 对象头(Header):对象头是本节内容会讨论重点讨论的部分。为了便于讨论,我们讨论32位JDK版本和32位操作系统下它的内部结构(64位JDK版本和64位操作系统的情况类似,只不过各主要结构都变成了32位的长度)。视情况它又可能分为2-3个子结构:

    • 数组长度(只有数组形式的对象会有这个区域):数组对象的这个区域表达了数组长度。

    • klass 这是一个指针区域,这个指针区域指向元数据区中(JDK1.8)该对象所代表的类描述,这样JVM才知道这个对象是哪一个类的实例

    • markword 区域是该对象关键的运行时数据,主要就是这个对象当前锁机制的记录信息。

注意:整个对象头的描述结构的长度并不是固定不变的,首先在32位操作系统和64位操作系统中就有结构长度上的差异。另外在启用的对象指针压缩和没有启用对象指针压缩的情况下,整个对象头的长度也不一样:64位平台下,原生对象头大小为16字节,压缩后为12字节。

这里我们重点讨论和synchronized加锁过程有关的markword区域,首先需要说明几点:

  • 根据对象所处的锁状态的不同,markword区域的存储结构会发生变动。例如当对象处于轻量级锁状态的情况下,markword区域的存储结构是一种定义;而当对象锁级别处于偏向锁状态的情况下,markword区域的存储结构又是另一种定义。

  • markword区域在64位JVM版本的情况和在32位JVM版本的情况下,其结构长度完全不一样。我们这里讨论64位的版本。以下示意图描述了32位JVM版本中常见的markword区域结构。为了让读者更清晰的理解这个结构,我对网上常见的图例资料进行了一些修改

在这里插入图片描述

2.1.2、synchronized关键字下的锁状态升级

通常情况下,我们在代码中使用synchronized关键字协调多个线程的工作过程,实际上就是使用Object Monitor控制对多个线程的工作过程进行协调。synchronized关键字的执行过程从传统的理解上就是“悲观锁”设计思想的一种实现。但实际上synchronized关键字的执行过程还涉及到锁机制的升级过程,升级顺序为 自旋锁、偏向锁、轻量级锁、重量级锁

  • 自旋锁:自旋锁实际上是一种基于CAS原理的实现方式(关于CAS原理在本专题之前的文章中已经介绍过,从根本上来说这是一个“乐观锁”设计思想的具体实现)。自旋锁就是在满足一定条件下,让当前还没有获得对象操作权的线程进入一种“自循环”的等待状态,而不是真正让这个线程释放CPU资源。这个等待状态的尝试时间和自旋的次数非常短,如果在这个非常短的时间内该对象还没有获得对象操作权,则锁状态就会升级。

    自旋锁的特点是,由于等待线程并没有切换出CPU资源,而是使用“自循环”的方式将自己保持在CPU L1/L2缓存中,这样就避免了线程在CPU的切换过程,在实际上并没有什么并发量的执行环境下减少了程序的处理时间。当基于当前对象的synchronized控制还处于“自旋锁”状态时,实际上并没有真正开启Object Monitor控制机制,所以这个自旋锁状态(包括偏向锁、轻量级锁)并不会反映在对象头的数据结构中

  • 偏向锁:偏向锁实际上是在没有多线程对指定对象进行操作权抢占的情况下,完全取消针对这个指定对象的同步操作元语。而当前唯一请求对象操作权的线程,将被对象记录到对象头中。这样一来如果一直没有出现其它线程抢占对象操作权的情况下,则当前同步代码块就基本上不会出现针对锁的额外处理。

    举一个栗子,当前进程中只有线程A在请求同步代码块X的对象操作权(对象记为Y),这时synchronized控制机制就会将Y对象的对象头记为“偏向锁”。这时线程A依然在执行同步代码块X时,又有另一个线程B试图抢占对象Y的操作权。如果线程B通过“自旋”操作等待后依然没有获取到对象Y的操作权,则锁升级为轻量级锁

  • 轻量级锁:按照之前对偏向锁的描述,偏向锁主要解决在没有对象抢占的情况下,由单个线程进度同步块时的加锁问题。一旦出现了两个或多个线程抢占对象操作时,偏向锁就会升级为轻量级锁。轻量级锁同样使用CAS技术进行实现,它主要说的是多个需要抢占对象操作权的线程,通过CAS的是实现技术持续尝试获得对象的操作权的过程。

    按照轻量级锁的定义,我们将以上的栗子继续下去。当前对象Y的锁级别升级为轻量级锁后,JVM将在线程A、线程B和之后请求获得对象Y操作的若干线程的当前栈帧中,添加一个锁记录空间(记为Key空间),并将对象头中的Mark Word复制到锁记录中。然后线程会持续尝试使用CAS原理将对象头中的Mark Word部分替换为指向本线程锁记录空间的指针。如果替换成功则当前线程获得这个对象的操作权;如果多次CAS持续失败,说明当前对象的多线程抢占现象很严重,这是对象锁升级为重量锁状态,并使用操作系统层面的Mutex Lock(互斥锁)技术进行实现。

// TODO 如下图所示,继续写

  • 重量级锁:当对象的锁级别升级为“重量级锁”时,JVM就开始采用Object Monitor机制控制各线程抢占对象的过程了。实际上这是JVM对操作系统级别Mutex Lock(互斥锁)的管理过程。

2.2、Object Monitor区域和工作过程

在这里插入图片描述

正所谓Object Monitor机制的名称一样,它就是以Java对象为基础的,在多线程环境下对特定对象的操作权限的一种控制方式。在这种控制方式中有三个象限:

  • 第一个象限为待进入监控区部分(Entry Set),停留在这个区域的线程由于还没有获得对象操作权限的原因,依然停留在synchronized同步块以外,具体来说就是synchronized(Object)这句代码的位置。处于“Entry Set”区域的线程,其线程状态被标识为BLOCKED

  • 第二个象限为对象操作权持有区,对于一个特定对象的Object Monitor控制来说,一个时间点最多有一个线程处于这个区域。也就是说一个时间点只可能有一个线程能拥有这个对象的操作权限。而当前持有对象操作权限的线程互斥量将被记录在这个对象的对象头中。

  • 另外请明确,本专题之前已经介绍过操作权和抢占权之间的关系:某一个线程通过wait等相关方法释放了对象的操作权限,但是只要这个线程没有退出synchronized同步块,就不会释放这个对象的抢占权。这是因为没有退出synchronized同步块,且暂时没有对象操作权限的线程都会被放置到待授权区域(Wait Set)——也就是上图所示的第三个象限区域。

  • 但是并不是处于待授权区(Wait Set)的线程都可以重新参与对象操作权的抢占,而是只有通过notify()或者相似方法被通知转移的线程能够参与。注意,每个对象的Object Monitor控制过程相对独立,但是一个线程可以同时拥有一个或者多个对象的操作权限

============================================================
(接下文)

原文地址:https://www.cnblogs.com/liulaolaiu/p/11744217.html