Mutex, Semaphore and Monitor (2)

    在上篇文章的最后,我们描述了CV(条件变量)的定义和使用方式,也曾说过Monitor事实上是基于CV的。那么,Monitor到底是怎样一种机制呢?

    其实,与其说Monitor是一种机制,倒不如说它是一种风格(style),因为它并不是一种新的同步机制。Monitor所做的,就是把mutexCV封装在一个对象里面,来保护这个对象的共有数据的访问,如下图所示:

其中Lock控制线程的进入,保证只有一个对象能拿到锁;而CV负责线程的等待、唤醒等操作;putget是对shared data的一组访问方法。这种形式就是Monitor

    看到这个模型,你可能马上就想到,这不就是Java里面 最常见的线程间同步机制么?没错,就是这样。JavaObject类提供了一套方法:wait/notify/notifyAll,并且通过在成员方法钱加synchronized方法来实现mutex,是典型的Monitor。而且,可以看出,MonitorJava最推荐使用的线程同步方式(使用关键字的形式和把wait/signal/broadcast实现在Object中,相比之下,信号量类就放在java.util.concurrent包里)。

    使用Monitor的好处在于,mutexCV对用户都是透明的。用户只需知道,处于synchronized保护下的代码都是互斥的,而线程在对象上进行等待或着被唤醒。所以,我们可以很方便的改写前一篇文章中,使用pthreadC程序。

public class Counter {

    private int count;
    
    public Counter(int count) {
        this.count = count;
    }
    public int get() {
        return this.count;
    }
    public void incr() {
        this.count++;
    }
}

class Checker implements Runnable {

    private Counter counter;
    
    public Checker(Counter c) {
        this.counter = c;
    }
    
    @Override
    public void run() {
    
        synchronized (counter) {
            System.out.println("The Checker get the lock and the count is " + counter.get());
            while(counter.get() < 10)
            try {
                counter.wait();
                System.out.println("The Checker is notified and re-get the lock, now the count is " + counter.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Increaser implements Runnable {

    private Counter counter;
    
    public Increaser(Counter c) {
        this.counter = c;
    }
    @Override
    public void run() {
    
        while (counter.get() < 100) {
            synchronized (counter) {
                System.out.println("Increaser get the lock and the count is "+ counter.get());
                counter.incr();
                if (counter.get() >= 10) {
                    counter.notifyAll();
                }
            }
        }
    }
}

  在main函数里面开启一个Checker和一个Increaser后运行,观察打印结果,会发现一个很有趣的现象,Checker的第二次打印,即在wait被唤醒之后打印的count并不是10,也就是说当Counter==10后,Increaser调用了notifyAll,但是Checker并没有恢复执行。为什么呢?原因我们在上篇文章中也提到过。notifyAll并不会释放锁,而是当Increaser离开synchronized代码块后才会释放,但是由于Increaser会循环执行(代码里是执行100次),它会和即将唤醒的Checker再次竞争mutex锁,所以并不能保证Checker会立即得到锁醒来,甚至,很多时候,Increaser更易得到mutex锁,使得checker醒来时counter远大于10。在我的实验中,有几次运行甚至是100,即Increaser执行完了,checker才得到mutex锁。

    解决这个问题也很简单,就是在notifyAll后面加上break,这样,Increaser会跳出循环,并且释放锁。当然,这是一个比较特殊的例子。更多的时候,比如说Increaser需要在Checker做完之后再处理一些任务呢?显然,我们可以在Increaser调用notifyAll之后,调用wait,让其释放锁并等待,然后在Checker恢复执行后,再唤醒Increaser。如此往复下去,我们可以实现让线程交叉执行,甚至然照一个指定的序列执行。关于这方面,可以看下《Thinking in Java》中,线程那一章一个洗车抛光的例子。

    讲到这里,相信大家对这三种机制已经有了一个大概的认识了,剩下的就得在实际工作中去体会和总结了,实践出真知嘛。

原文地址:https://www.cnblogs.com/tobealion/p/4555488.html