操作系统深入学习-----重新认识Synchronized的实现原理,管程Monitor介绍

一.从一道面试题说起

请你说一下Synchronized的实现原理?

  • 这个面试题可是太常见了,一般来说会怎么回答呢?
  • synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。在 Java 6 之前,monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在 Java 6 的时候,Java 虚拟机 对此进行了大刀阔斧地改进,提供了三种不同的 monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。

这个解答没有问题,但仅仅从字节码的层面来看Synchronized的实现是不正确的,当然,这个回答也提到了锁升级的概念,这个概念在我的文章锁升级中有解释


二.Monitor管程是什么?

管程是由局部于自己的若干公共变量及其说明和所有访问这些公共变量的过程所组成的软件模块。管程属于编程语言级别的互斥解决方案,最早是Brinch Hanson和Hoare于1970s提出的概念,已在Pascal、Java、Python等语言中得到了实现。

1.由来

  1. 管程,英文是 Monitor,也常被翻译为“监视器”,monitor 不管是翻译为“管程”还是“监视器”,都是比较晦涩的,通过翻译后的中文,并无法对 monitor 达到一个直观的描述。
  2. 在《操作系统同步原语》 这篇文章中,介绍了操作系统在面对 进程/线程 间同步的时候,所支持的一些同步原语,其中 semaphore 信号量 和 mutex 互斥量是最重要的同步原语。
    在使用基本的 mutex 进行并发控制时,需要程序员非常小心地控制 mutex 的 down 和 up 操作,否则很容易引起死锁等问题。为了更容易地编写出正确的并发程序,所以在 mutex 和 semaphore 的基础上,提出了更高层次的同步原语 monitor,不过需要注意的是,操作系统本身并不支持 monitor 机制,实际上,monitor 是属于编程语言的范畴,当你想要使用 monitor 时,先了解一下语言本身是否支持 monitor 原语,例如 C 语言它就不支持 monitor,Java 语言支持 monitor。
  3. 一般的 monitor 实现模式是编程语言在语法上提供语法糖,而如何实现 monitor 机制,则属于编译器的工作,Java 就是这么干的。

总结:操作系统内核的线程同步机制有semaphore和mutex(互斥锁和信号量),但是在使用它们的时候可能会出现一些时序的错误,当利用它们来解决临界区问题(哲学家进餐,生产者-消费者等)会产生各种类型错误。研究者因此提出了一种高级语言构造,这就是管程Monitor的由来


2.特点

  1. monitor 的重要特点是,同一个时刻,只有一个 进程/线程 能进入 monitor 中定义的临界区,这使得 monitor 能够达到互斥的效果。但仅仅有互斥的作用是不够的,无法进入 monitor 临界区的 进程/线程,它们应该被阻塞,并且在必要的时候会被唤醒。显然,monitor 作为一个同步工具,也应该提供这样的管理 进程/线程 状态的机制。想想我们为什么觉得 semaphore 和 mutex 在编程上容易出错,因为我们需要去亲自操作变量以及对 进程/线程 进行阻塞和唤醒。monitor 这个机制之所以被称为“更高级的原语”,那么它就不可避免地需要对外屏蔽掉这些机制,并且在内部实现这些机制,使得使用 monitor 的人看到的是一个简洁易用的接口。
  2. Monitor本质上是对通用同步工具的一种抽象,它就像一个线程安全的盒子,用户程序把一个方法或过程(代码块)放进去,它就可以为他们提供一种保障:同一时刻只能有一个进程/线程执行该方法或过程,从而简化了并发应用的开发难度。

3.组成

monitor 机制需要几个元素来配合,分别是:

  1. 临界区

  2. monitor 对象及锁

  3. 条件变量以及定义在 monitor 对象上的 wait,signal 操作。

使用 monitor 机制的目的主要是为了互斥进入临界区,为了做到能够阻塞无法进入临界区的 进程/线程,还需要一个 monitor object 来协助,这个 monitor object 内部会有相应的数据结构,例如列表,来保存被阻塞的线程;同时由于 monitor 机制本质上是基于 mutex 这种基本原语的,所以 monitor object 还必须维护一个基于 mutex 的锁。
此外,为了在适当的时候能够阻塞和唤醒 进程/线程,还需要引入一个条件变量,这个条件变量用来决定什么时候是“适当的时候”,这个条件可以来自程序代码的逻辑,也可以是在 monitor object 的内部,总而言之,程序员对条件变量的定义有很大的自主性。不过,由于 monitor object 内部采用了数据结构来保存被阻塞的队列,因此它也必须对外提供两个 API 来让线程进入阻塞状态以及之后被唤醒,分别是 wait 和 notify。


三.Java语言中Synchronized的底层实现原理


  1. 从Monitor角度来看,Java monitor机制通过synchronized关键字暴露给用户,syncronized可以修饰方法或代码块(两者本质上都是一个过程)。
  2. 从Java语言角度来看,syncronized作为关键字,会制定一个对象(实例对象或者class对象),作为Monitor Object,这个对象的对象头中会记录Monitor Record的地址。

操作系统深入学习-----重新认识Synchronized的实现原理,管程Monitor介绍

Monitor Record

每一个被锁住的对象都会和一个monitor record关联(对象头的MarkWord中的LockWord指向monitor record的起始地址),同时monitor record中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。如下图所示为Monitor Record的内部结构。

操作系统深入学习-----重新认识Synchronized的实现原理,管程Monitor介绍

  • Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL
  • EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。
  • RcThis:表示blocked或waiting在该monitor record上的所有线程的个数。
  • Nest:用来实现重入锁的计数
  • HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)
  • Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。

两个Set

java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖于一个叫 ObjectMonitor 模式的实现,这是 JVM 内部基于 C++ 实现的一套机制,对操作系统内核的Monitor进行了封装。基本原理如下所示:
操作系统深入学习-----重新认识Synchronized的实现原理,管程Monitor介绍
当一个线程需要获取 Object 的锁时,会被放入 EntrySet 中进行等待,如果该线程获取到了锁,成为当前锁的 owner。如果根据程序逻辑,一个已经获得了锁的线程缺少某些外部条件,而无法继续进行下去(例如生产者发现队列已满或者消费者发现队列为空),那么该线程可以通过调用 wait 方法将锁释放,进入 wait set 中阻塞进行等待,其它线程在这个时候有机会获得锁,去干其它的事情,从而使得之前不成立的外部条件成立,这样先前被阻塞的线程就可以重新进入 EntrySet 去竞争锁。这个外部条件在 monitor 机制中称为条件变量。


总结

Synchornized并不等同于Monitor,而是Monitor机制包括Synchornized,和一系列线程调用的API比如Wait(),Notify()方法。开发者要结合使用 synchronized 关键字,以及 Object 的 wait / notify 等元素,才能说自己利用 monitor 的机制去解决了一个生产者消费者的问题。