本文版权归作者所有,如有转载请与作者联系并注明出处http://blog.csdn.net/Iangao/archive/2008/10/09/3044972.aspx。
管程(Monitor)是一种抽象数据类型,它包含一个存储定间,还有一组用于控制对这组存储空间进行访问的私有方法,它可以保证在任意时刻存储空间只能被一个可以执行这组私有方法的进程访问。有数据,有方法,看起来很象类,只不过这个类的方法是要独占访问整个数据对象的,而上面所说的私有方法也很象是用于控制临界区的锁方法,只是这个加锁和解锁的过程是由管程内置执行的。所以我们可以把它看成是对临界区定义的一种OOP抽象。我们可以估且把它按照下面的代码清单来理解:
monitor{ private: int mutex=1; // 私有同步信号(系统实现) private: // 私有方法(系统实现) lock(int mutex){ ... } unlock(int mutex){ ... } public: proc_i(...){ lock(mutex); // 加锁(系统实现) ... unlock(mutex); // 解锁(系统实现) } } | /** * 使用Monitor后的效果 */ monitor sharedBalance{ private: //管程中被同步的数据 int balance; public: // 因为下面的方法进出之时都会被系统自动的加解锁了 // 所以无需再去考虑锁的处理。 credit(int amount){balance+=amount;} // 贷 debit(int amount){balance-=amount;} // 借 } |
通过对上面伪码的分析我们可以看出, 通过管程实现了对临界区自动的定义及管理,使得代码更加清晰易读了。也可以让我们把注意力集中到方法的业务逻辑上了。
回顾一下synchronized的用法我们不难看出, 第二种用法正是按照了管程的定义实现的,那么我们可以把所有声明了同步方法的类看成是管程类。其实不只是第二种用法,第一种用法实际上也可以重构成以管程来表示的形式(参看下面的代码清单)。这时我们可以把小括号内的对象看成是管程对象,而把大括号内的代码看成是该管程对象内的一个匿名的同步方法。
/** * @author iangao | /** * 按照管程来看待左边的代码。 * @author iangao */ public class Foo { private double balance, amount; //共享资源 private SynObject synObject=new SynObject() //同步对象 public void f(){ ... synObject.prc1(); // 重构后的临界区代码调用 ... } public void g(){ ... SynObject.prc2(); ... } /** * 重构后的同步对象 */ public class SynObject=new Object(){ public synchronized void prc1(){ // 临界区A balance=balance+amount; } public synchronized void prc2(){ // 临界区B balance=balance+amount; } }; } |
2.3.1.1 问题的提出:
通过前面的讨论我们解了临界区以及锁的工作原理,由于临界区是原子的那么当我们在临界区执行的过程中由于某些条件无法满足,需要等其他进程/线程对临界区锁定的资源进行一些处理使得条件满足后再继续执行时。就需要先释放对资源的锁,并等待条件满足后再重新锁定资源继续执行。否则系统将很容易进入死锁状态。而在一同一资源的不同临界区内可能还会有几种与不同条件相关联的等待点存在着。根据上面的问题描述我们可以看出这个等待点是有与一个条件(condition)相关联的,条件满足时我们要求只唤醒那些等待此条件的线程,而等待其他条件的线程还要继续等待。
我们可以参看下面的代码清单来理解这个问题:
shared double balance, account; //共享变量 shared int lock=FALSE; //同步变量(Mutex) shared int condition=FALSE; //条件变量 | |
Program for P1 ... enter(lock); // 1. 锁 ... balance=balance+account; // 2. 执行P1临界区中的代码 ... while(!condition){ exit(lock); // 3. 条件不满足,释放锁 ---------------------------------------------------------> wait(condition); // 4. 等待条件被满足 enter(lock); // 7. 条件已满足,等待P2释放锁 } // 9. 获取P2释放的锁 doAfterCondition() // 10. 执行满足条件后的操作 exit(lock); // 11. P1结束,解锁 | Program for P2 ... enter(lock); // 2. 等待P1解锁 ... // 4. 获取P1释放的锁 balance=balance-account; condition=TRUE; // 5. 设置条件 signal(condition); // 6. 通知P1条件已满足 <------------------------------------------------- .... // 7. 继续P2后续的任务 exit(lock); // 8. P2结束,解锁 <------------------------------------------------- |
2.3.1.2 条件变量
我们要解决的是从临界区中途释放临界区的控制权(暂停线程)待到条件成熟后再唤醒线程重回临界区(重获控制权)继续执行的问题。既然是临界区内的问题,而管程又是处理临界区的方案中比较好用的一种,那么我们就考虑创建一种管程内的数据结构,它对管程的所有同步方法都是可见的。通过它来完成前面提到的等待、唤醒等功能。因为等待和唤醒往往和某一条件的达成与否相连系,所以我们把这种数据结构就叫做条件变量(Condition Variable).
条件变量一般情况下包含三个方法:
- 1.wait():释放管程并挂起调用进程,直到另一个进程调用了signal()方法,.
- 2.signal():如果某个进程由于wait()而被挂起,那么就唤醒这个进程。如果没有被进程在等待,那么这个信号将被丢弃。(即没有任何作用)
- 3.queue():返回等待条件的线程的数量
根据上面的定义我们可以看出,我们可以把条件变量当成是一个在临界区内使用的特殊的事件(Event),因为它们要实现的功能是一样的,只是多了一个释放和恢复管程控制权的操作。另外,我们可以看到条件变量的加入以及同一管程可以有多个条件变量的特性,使我们得以按照业务需要将对同一资源的等待进程分别在不同的条件变量上排队,使得我们可以对资源的访问进程进行分组控制。
下面,我们试着用管程和条件变量来解决上述问题,代码清单如下:
monitor conditionTest{
private:
double balance, account; // 共享变量
int flag=FALSE; // 条件
condition c; // 条件变量
public:
programForP1();
programForP2();
}programForP1(){ // <临界区A>
...
while(!flag) c.wait(); // 1. wait,等待flag=true
---------------------------------------------------------->
balance=balance+account; // 6. 继续执行
}programForP2(){ // <临界区B>
...
balance=balance-account; // 2.P2代码
flag=true; // 3.设置 flag
c.signal(); // 4.唤醒P1
... // 5.P2代码
}
<--------------------下图显示了显示了现实生活中共享资源争用的例子。如图所示,所有的车(线程)都要通过十字路(临界区),然而两条路上的车队(线程队列)都要等待信号灯(条件变量的信号),如果现在有1线的信号灯,那么1号线的车就可以通行,而2号线中的车还必须等待.由此可见每个信号灯(条件变量)只对一条线路上的车辆起作用(只能唤醒等待此条件变量的线程).
1 Java中的Object类
通过前面对synchronized关键字的讨论,我们可以得出一个结论就是“所有被synchronized关键同步的对象(Object)都可以看做是一个管程(Monitor)”。而Java语言也在所有Java类的基类Objcet类中为我们准备了几个用于线程间调度的方法。wait(),notify(),notifyAll()。这几个方法必须用在synchronized标识所在对象的临界区内使用, 也就是说必须在当对象作为管程对象时才能使用,否则就会抛出“无效的管程状态异常” java.lang.IllegalMonitorStateException。它们的作用分别是:
- wait(); 释放同于同步而被独占的管程对象的控制权,并挂起发启者线程。
- notify(): 唤醒一个由于调用wait()而处于等待中的线程。如果没有线程就忽略此操作。
- notifyAll(): 唤醒所有由于调用wait()处于等待中的线程。如果没有线程就忽略此操作。当然最后只有一个会获得管程的控制权。
大家注意到了这些方法所提供的功能以及使用的条件都很象条件变量。实际上对于POSIX系统的条件变量方法wait(), timed_wait(), signal(), broadcast()提供的功能和就直接对应Java中wait(), wait(long), notify(), notifyAll()方法所提供的功能。它们是的功能是一致的, 只是它只能完成1个条件变量的任务,如果要实现在一个管程中两个以上条件变量协调工作的功能就不行了,也就是说不论什么条件下wait()的线程都处于一个等待队列中,而notify()时也无法有选择的唤醒符合某一条件的一组等待线程.所以我们可以认为wait(),notify()等方法都是对sycnhonized同步锁内的一个匿名的条件变量的调用。
2. 锁接口(Lock)
前面我们讨论过,虽然synchronized关键字可以满足我们大部分的同步需求.但是还是有一些特殊的情况, 比如在本节我们要讨论的"条件变量"就需要在两个不同的方法中去完成加锁和解锁操作,这时sychronized就不适用了.因此我们还是有必要重新抽象出一个Lock接口类去完成与synchronized相同的功能(进一步研究我们会发现临界区的锁有很多种,如Mutex互斥锁、读写锁等,因此这里我们抽象的是Lock接口而不是一个具体的类),这个锁应当具有如下所示的几个接口方法:
/**
* 锁(临界区)接口
* @author iangao
*/
public interface Lock {
/**
* 加锁(进入临界区)
* @param mills 等待mills时间
*/
boolean lock(long mills) throws InterruptedException;
/**
* 加锁(进入临界区)
*/
boolean lock() throws InterruptedException;
/**
* 解锁(离开临界区)
* @throws InterruptedException
*/
void unlock();
/**
* 放弃锁(临界区)控制权
* @return 反回当前锁的层数,用于恢复时使用
*/
long relinquish();
/**
* 恢复锁(临界区)控制
* @param holdCount 恢复放弃之前的加锁层数
*/
void restore(long holdCount) throws InterruptedException;
}2 条件变量的Java实现
通过前面的分析我们可以看出Condition实际上就是管程状态下的Event应用,在wait操作中多出了对管程控制权的释放和恢复功能。因此下面的实现中我们直接通过继承Event类来实现Condition. 代码清单如下:
/**
* 条件变量 --- 管程内的事件(Event)
* @author iangao
*/
public class Condition extends Event{
private Lock lock; // 条件所在临界区锁
/**
* 与一个锁(管程)相关联
* @param mutex 互斥锁
*/
public Condition(Lock lock){
this.lock=lock;
}
/**
* 等待条件满足信号
* @param millis 等待时间,0表示死等
*/
public void await(long millis) throws InterruptedException{
long holdCount=lock.relinquish(); // 放弃临界区,记录锁层数
boolean success=super.await(millis); // 等待条件信号
if(!success) return false; // 重获控制权失败
lock.restore(holdCount); // 恢复临界区状态,直接加holdCount层
return success;
}
}3. 扩展Mutex类
由于Mutex类本身就是实现锁功能的,因此本节对其进行扩展,使其包含Lock接口.这样Mutex就可以做为Lock使用了.扩展后的Mutex类如下
/**
* 互斥信号量
* @author iangao
*/
public class Mutex implements Lock{
......
public boolean lock(long mills) throws InterruptedException{
return p(mills,1);
}
public boolean lock() throws InterruptedException{
return lock(0);
}
public void unlock(){
v(false);
}
public long relinquish() {
return v(true);
}
public void restore(long holdCount) throws InterruptedException {
p(0,holdCount);
}
}4 测试条件变量的应用
下面我们演示一下使用这套机制实现的条件变量是如何工作的.如下所示,我们要创建3个线程,并做如下测试:线程1等待条件A,线程2等待条件B,线程3唤醒等待条件B的线程(即线程2),接下来线程2可以执行,而线程1将继续等待。
public class ConditionTest {
public static void main(String[] args){
new ThreadsTest(){
private Lock lock=new Mutex();
private Condition conditionA=new Condition(lock);
private Condition conditionB=new Condition(lock);
/**
* 线程1: 等conditionA信号
*/
public void runInThread1() throws InterruptedException{
try {
lock.p();
System.out.println("[T1]: 等待conditionA ...");
conditionA.await();
System.out.println("[T1]: 获得conditionA信号");
} finally{
lock.v();
}
}
/**
* 线程2: 等conditionB信号
*/
public void runInThread2() throws InterruptedException{
try{
lock.p();
output("[T2]: 等待conditionB ...");
conditionB.await();
output("[T2]: 获得conditionB信号");
} finally{
lock.v();
}
}
/**
* 线程3: 发出ConditionB信号
*/
public void runInThread3() throws InterruptedException{
try {
lock.p();
output("[T3]: 执行一些操作(2秒)");
Thread.sleep(2000);
output("[T3]: 发出conditionB信号(2秒)");
conditionB.signal();
Thread.sleep(2000);
output("[T3]: 发出conditionB信号后的操作");
}finally{
lock.v();
}
}
}.execute(3);
}
}测试结果:
[T1]: 等待conditionA ...
[T2]: 等待conditionB ...
[T3]: 执行一些操作(2秒)
[T3]: 发出conditionB信号(2秒)
[T3]: 发出conditionB信号后的操作
[T2]: 获得conditionB信号 // 等T3释放管程后才会响应,因为condition要重获管程控制权在上面的例子中,我们实现了Thread1为在ConditionA队列上等待,Thread2在ConditionB队列上等待,面Thread3则是用于唤醒ConditionB上等待对队列的,而mutex在此处起到管程的作用,从而保证Thread1,Thread2,Thread3三个线程在执行时间上是互斥的。
4. J2SE 1.5中的条件变量
在1.5版中提供了一个java.util.concurrent.locks.Condition的接口.该接口通过await()方法实现等待信号功能,通过signal()和signalAll()方法实现发送唤醒信号功能。与wait()、notify()是与sychronized关键字绑在一起使用类似,Condition接口也是与前面提到的java.util.concurrent.locks.Lock接口类绑在一起的。这节我们关注的是Lock中的newCondition()方法,因为它可以创建出一个与Lock对象相关的Condition对象, 而且此Condition对象只能在使用Lock接口中lock()和unlock()方法划定出的临界区内使用。