Java并发 - 管程相关的思考和总结

管程,对应的英文是 Monitor,很多同行翻译成 “监视器”,这是直译,操作系统领域一般都翻译成 “管程”,这是意译,我更喜欢后者。

在管程的发展史上,先后出现了三种管程模型,分别是:Hasen 模型, Hoare 模型 和 MESA 模型,Java 管程实现参照的是如今广泛应用的 MESA模型。

Java 里管程的实现有两种 SynchronizedReentrantLock

要了解一个新东西,首先要明确它解决的问题,并发编程里有两个核心的问题:

  • 互斥:同一时刻只能有一个线程访问共享资源;
  • 同步:线程间如何通信、协作。

先来看第一问题,互斥:

得益于 java OOP(具体是封装)的编程特性,这个问题处理起来并不难:只需要将共享变量及其对共享变量的操作封装起来!

想想 java 中 Collections.synchronizedList() 是如何是实现线程安全容器的:

public E get(int index) {
    synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
    synchronized (mutex) {return list.set(index, element);}
}

封装一个 List(局部变量,对应管程模型的共享变量),然后对 get()&set() 加锁(对应管程模型中 对共享变量的操作)。

同理,下面是 MESA 模型解决互斥问题的结构图:

Monitor X{
    // 私有共享变量:队列
    var queue;
    // 入队
    func enq();
    // 出队
    func deq(); 
}

你会发现:管程模型和面向对象是高度契合的

重点看下第二个问题,同步:

扫描二维码关注公众号,回复: 12609345 查看本文章

解决同步问题的具体实现,也就是我们常用的三个方法:

  • wait()
  • notify()
  • notifyAll()

先上一个管程解决同步 的流程图:

这里管程的结构为:共享变量V,条件变量AB,条件变量各自对应的等待队列ab,入口等待队列in,以及各种方法(wait(),notify(),notifyAll() 这里没有标出)

我们来模拟一个场景:

1) 线程 T1 首次请求锁,这时所有条件都满足,于是通过图中的 路径1(标红),获取了共享变量V;

2) T1 还没释放锁的时候,T2 请求获取锁,这是条件不满足(T1 正在占用V),于是 T2通过路径2(标红),进入了AB其中一个条件变量的等待队列ab,等待,这个过程调用的方法是 T2调用wait();

3) T1 释放锁,T2 的条件满足了,T2 通过路径3(标红),进入入口等待队列in,准备在次获取锁,这个过程调用的方法是 T1 调用 notify() 或 notifyAll()

关于notify() 和 notifyAll():

  • notify():唤醒条件变量等待队列的任意一个线程;
  • notifyAll():唤醒条件变量等待队列的所有线程;

《Effictive Java 3rd》item81:PREFER CONCURRENCY UTILITIES TO WAIT AND NOTIFY 有这样一句话:

It is Sometimes said that you should always use notifyAll.This is reasonable.conservative advice.

一种常见的说法是,你总是应该使用 notifyAll(),这是一条合理且保守的建议。

我的理解是:

如果你的锁没有经过深思熟虑的 细化处理(一把锁只对应一个资源,也可以理解为请求该锁的线程需要满足的条件都相同),你都只能用 notifyAll()。因为,一把锁对应多个资源,如果你用 notify(),随机唤醒了 等待队列中的一个线程,而这个线程请求的资源刚好没有被释放(条件变量不满足),所有这个线程最终又回到了 条件变量等待队列。导致了 条件变量等待队列 中永远都会有一个线程无法被 notify(),于是就形成了死锁!

 

关于ReentrantLock是否是重复造轮子的思考:

有人会问:管程的实现既然已经有了 synchronized 为什么后来又发布了 ReentrantLock?重复造轮子?

答案肯定是否定的!

回到上面的死锁问题:错误使用 notify() 导致了 条件变量 对应的 条件变量等待队列中的线程 永远无法获取资源 从而永远无法被唤醒!我们不禁想,要是有办法让长时间无法获取锁的线程自己释放资源,上面的死锁就可以避免了!你可能已经想到了下面的代码:

ReentrantLock lock = new ReentrantLock();
// 支持超时
lock.tryLock(1000, TimeUnit.MICROSECONDS);

支持超时!是的,这是 ReentrantLock 出现的理由之一,同时还有另外两个理由:

// 支持中断
lock.lockInterruptibly();
// 支持非阻塞异步获取锁
lock.tryLock();

 

猜你喜欢

转载自blog.csdn.net/weixin_41346635/article/details/114171210