管程和信号量分析

TOP 带着问题来分析

  1. 管程和信号量区别
  2. 为什么 Mesa 模型的 wait() 支持超时参数

1. 管程(Monitor)

管程也被称为监视器,指的是通过管理共享变量以及对共享变量的操作过程,实现了在一个时间点,最多只有一个线程在执行(线程安全的,支持并发)。

管程与信号量是等价的,信号量我们下面会有介绍,管程相比信号量来说,其隐蔽了同步的细节,更易于用户维护,而信号量的 PV 操作会大量分散到代码的地方,不易维护容易造成死锁,所以 Java 选择了管程(面向对象方法)。

回到问题 TOP 1 ,可以明白其区别主要是在面向程序员的维护与体验。

我们来举一个场景,线程 T1 去操作管程中的共享数据,但是因为还不满足 “条件”(例如条件是队列不空),这个时候 T1 入条件对应的等待队列;假设之后线程 T2 入队成功,满足了 T1 的 “条件”。这个时候 T2 是先唤醒 T1 再执行还是自己执行完再唤醒呢?

针对这个场景,我们来看管程的三种模型:

1.1 Mesa 模型

Mesa 模型是现在广泛应用的模型,Java 管程也是参考的该模型。可以看到在处理以上场景时,也就是图中第 4 步,T1 线程被唤醒后会从 “条件” 等待队列转移到入口队列,T2 线程会继续执行,最后 T1 从入口队列出来继续执行。

img

1.2 Hoare 模型

与 Mesa 模型不同的是,Hoare 模型在处理这个场景时,同样在第 4 步是把 T2 线程放入一个 Signal 队列等待唤醒,T1 线程执行完会去 Signal 队列唤醒 T2(如果 Signal 队列为空才去入口队列调度),T2 重新开始。

img

1.3 Brinch Hanson 模型

该模型仅允许线程完成(从 Monitor 退出时)发出信号唤醒,也即是第 4 步 T2 线程会一直执行完再去唤醒 T1 。

img

三种模型中,第一种 Mesa 模型的 wait() 是支持超时参数,因为 Mesa 模型中唤醒后进入的是入口等待队列,不一定执行,而后两种模型唤醒后是会直接调度的,所以不需要超时时间。

回到问题 TOP 2 ,可以明白其参数的意义。

相对来说Hoare 模型增加了一个队列(类似优先队列)成本较高,Brinch Hanson 模型较为简单,而且唤醒对比 Mesa 模型保证能一定执行。

对于 JAVA 层面的管程实现 AQS,可以参考后面几篇源码分析。

2. 信号量(Semaphore)

并发编程领域的大师 Edsger Dijkstra 提出了一种经典的解决同步不同执行线程问题的方法,这个方法是基于一种叫做 信号量 的特殊类型变量的。它由两种特殊的操作来处理,这两种操作称为 P 和 V。

  • P(s) 操作

    P(s) 把共享变量(信号量) s 减 1,并且立即返回。如果 s 为 0,则挂起这个线程直到变成非0

  • V(s) 操作

    V(s) 把 s 加 1。如果有线程阻塞在 P 操作等待 s 变成非 0,V 操作会唤醒这些线程中的一个,然后把 s 减 1(P 操作)

P V 操作看起来很抽象,我们举个现实中采用信号量控制线程的例子,例如我们信号量设置的是2,也就是同时只允许2个线程处理,当第三个线程 T3 来的时候 T1、T2 还没处理完的情况下,T3 会阻塞到 T1 或 T2 执行完成并且通过 V 操作(加1,释放一个位置给别人),这个时候 T3 进行 P操作(减一,把这个位置占用)。

同样的如果s我们设置为1,则可以实现线程之间的互斥操作。

关于 Java 版本的信号量实现可以参考后面几篇源码分析。