Java 语言在 1.5 之前, 唯一提供的 并发原语 是 管程
在 Java 1.5 提供的 JUC 包中, 也是以 管程 技术为基础的
管程是一把解决并发问题的万能钥匙
管程
在 Java 1.5 之前, 仅仅提供 synchronized 关键字和 wait/notify/notifyAll 方法
Java 采用的是 管程 技术, synchronized 关键字以及 wait/notify/notifyAll 方法都是 管程的组成部分
管程和信号量是等价的 (即用管程能实现信号量, 用信号量也能实现管程), 但管程 更容易使用 , 所以 Java 选择了管程
Monitor , 在 Java 领域会翻译成 监视器 , 在 操作系统 领域会翻译成 管程
管程: 管理共享变量以及对共享变量的操作过程, 让它们支持并发
对应 Java 领域: 管理类的 成员变量 和 成员方法 , 让这个类是 线程安全 的
MESA 模型
在管程的发展史上, 先后出现了三种不同的管程模型, 分别是: Hasen 模型, Hoare 模型和 MESA 模型
现在广泛应用的是 MESA 模型, Java 管程的实现也参考了 MESA 模型
管程可以解决并发领域的两大 核心 问题: 互斥 + 同步
互斥 : 在同一时刻 只允许一个线程 访问共享资源
同步 : 线程之间如何 通信 , 协作
互斥
管程解决互斥问题的思路: 将 共享变量以及对共享变量的操作 统一封装起来
管程 X 将共享变量 queue 和相关的操作 enq()和 deq()都封装起来
线程 A 和线程 B 如果想要访问共享变量 queue, 只能通过调用管程 X 提供的 enq()和 deq()方法来实现
enq()和 deq()保持互斥性, 只允许一个线程进入管程 X
管程模型与面向对象高度契合
同步
在管程模型里, 共享变量和对共享变量的操作是被封装起来的, 最外层的框是代表封装的意思
框的上面只有一个入口, 并且在入口旁边还有一个 入口等待队列
当多个线程同时试图进入管程内部时, 只允许一个线程进入, 其他线程就在 入口等待队列 中等待
管程里还引入了 条件变量 的概念, 每个条件变量都对应一个等待队列
条件变量和其对应等待队列的作用: 线程同步
实例: 出队入队
- // 下列三对操作的语义是相同的
- // Condition.await() Object.wait()
- // Condition.signal() Object.notify()
- // Condition.signalAll() Object.notifyAll()
- public class BlockedQueue<T> {
- private static final int MAX_SIZE = 10;
- // 可重入锁
- private final Lock lock = new ReentrantLock();
- // 条件变量: 队列不满
- private final Condition notFull = lock.newCondition();
- // 条件变量: 队列不空
- private final Condition notEmpty = lock.newCondition();
- // 队列实际存储: 栈
- private final Stack<T> stack = new Stack<>();
- // 入队
- public void enq(T t) {
- // 先获得互斥锁, 类似于管程中的入口
- lock.lock();
- try {
- while (stack.size()>= MAX_SIZE) {
- // 队列已满, 等待队列不满, 才可入队
- notFull.await();
- }
- // 入队后, 通知队列不空, 可出队
- stack.push(t);
- notEmpty.signalAll();
- } catch (InterruptedException ignored) {
- } finally {
- lock.unlock();
- }
- }
- // 出队
- public T deq() {
- // 先获得互斥锁, 类似于管程中的入口
- lock.lock();
- try {
- while (stack.isEmpty()) {
- // 队列已空, 等待队列不空, 才可出队
- notEmpty.await();
- }
- // 出队后, 通知队列不满, 可入队
- T pop = stack.pop();
- notFull.signalAll();
- return pop;
- } catch (InterruptedException ignored) {
- } finally {
- lock.unlock();
- }
- return null;
- }
- }
假设线程 T1 执行出队操作, 执行出队操作的前提条件是队列不空, 而 队列不空 就是管程里的 条件变量
如果线程 T1 进入管程后恰巧发现队列为空, 就会到 队列不空这个条件变量的等待队列 里等待
当线程 T1 进入条件变量的等待队列后 , 是 允许其他线程进入管程 的
再假设线程 T2 执行入队操作, 执行成功后, 队列不空这个条件对于线程 T1 来说是已经满足了的, 线程 T2 会通知线程 T1
当线程 T1 得到通知后, 会从 等待队列 里面出来, 但 不能马上执行 , 需要重新进入到 入口等待队列
编程范式
对于 MESA 管程 , 有一个编程范式:
while(条件不满足){wait();}
, 这是 MESA 管程 特有 的
Hasen 模型, Hoare 模型和 MESA 模型的 核心 区别: 当条件满足时, 如何通知相关线程
管程要求同一时刻只允许一个线程执行, 当线程 T2 的操作使线程 T1 等待的条件满足时
Hasen 模型 : 要求 notify()放在 代码的最后 , 这样 T2 通知完 T1 后, T2 也就结束了, 然后 T1 再执行
缺点: 不灵活
Hoare 模型 :T2 通知完 T1 后, T2 阻塞, T1 马上执行, 等 T1 执行完, 再唤醒 T2
缺点: 相比 Hasen 模型模型, 多了一次阻塞唤醒操作
MESA 模型 :T2 通知完 T1 后, T2 接着执行, T1 不会立即执行, 仅仅是从 条件变量的等待队列 进入到 入口等待队列
优点: notify()不用放在代码的最后, 也没有多余的唤醒阻塞操作
缺点: 当 T1 再次执行的时候, 曾经满足的条件可能已经不满足了 , 所以才有上面特有的编程范式
notify 的使用场景
一般情况下, 尽量使用 notifyAll()
满足 3 个条件, 也可以使用 notify()
所有等待线程拥有 相同的等待条件
所有等待线程 被唤醒后执行相同的操作
只需要唤醒一个线程
Java 的管程实现
Java 参考了 MESA 模型, 语言内置的管程 (synchronized) 对 MESA 模型进行了 精简
在 MESA 模型中, 条件变量可以有多个, 但 Java 语言内置的管程只有一个条件变量
Java 内置的管程方案 (synchronized) 使用很简单
synchronized 关键字修饰的代码块, 在 编译期 会自动生成相关加锁和解锁的代码, 但 仅支持一个条件变量
JUC 包实现的管程 支持多个条件变量 (例如 ReentrantLock), 但需要开发人员手动进行加锁和解锁操作
来源: http://www.tuicool.com/articles/aIvAFbF