转自:http://www.cnblogs.com/tomsheep/archive/2010/06/09/1754419.html

1. 什么是Monitor?

Monitor其实是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象,主要特点是:

  • 对象的所有方法都被“互斥”的执行。好比一个Monitor只有一个运行“许可”,任一个线程进入任何一个方法都需要获得这个“许可”,离开时把许可归还。
  • 通常提供singal机制:允许正持有“许可”的线程暂时放弃“许可”,等待某个谓词成真(条件变量),而条件成立后,当前进程可以“通知”正在等待这个条件变量的线程,让他可以重新去获得运行许可。

Monitor对象可以被多线程安全地访问。关于“互斥”与“为什么要互斥”,我就不傻X兮兮解释了;而关于Monitor的singal机制,历史上曾经出现过两大门派,分别是Hoare派和Mesa派(上过海波老师OS课的SS同学应该对这个有印象),我还是用我的理解通俗地庸俗地解释一下:

  • Hoare派的singal机制江湖又称“Blocking condition variable”,特点是,当“发通知”的线程发出通知后,立即失去许可,并“亲手”交给等待者,等待者运行完毕后再将许可交还通知者。在这种机制里,可以等待者拿到许可后,谓词肯定为真——也就是说等待者不必再次检查条件成立与否,所以对条件的判断可以使用“if”,不必“while”
  • Mesa派的signal机制又称“Non-Blocking condition variable”, 与Hoare不同,通知者发出通知后,并不立即失去许可,而是把闻风前来等待者安排在ready queue里,等到schedule时有机会去拿到“许可”。这种机制里,等待者拿到许可后不能确定在这个时间差里是否有别的等待者进入过Monitor,因此不能保证谓词一定为真,所以对条件的判断必须使用“while”

这两种方案可以说各有利弊,但Mesa派在后来的盟主争夺中渐渐占了上风,被大多数实现所采用,有人给这种signal另外起了个别名叫“notify”,想必你也知道,Java采取的就是这个机制。

2. Monitor与Java不得不说的故事

子曰:“Java对象是天生的Monitor。”每一个Java对象都有成为Monitor的“潜质”。这是为什么?因为在Java的设计中,每一个对象自打娘胎里出来,就带了一把看不见的锁,通常我们叫“内部锁”,或者“Monitor锁”,或者“Intrinsic lock”。为了装逼起见,我们就叫它Intrinsic lock吧。有了这个锁的帮助,只要把类的所有对象方法都用synchronized关键字修饰,并且所有域都为私有(也就是只能通过方法访问对象状态),就是一个货真价实的Monitor了。比如,我们举一个大俗例吧:

 Example Source Code [http://www.cnblogs.com/tomsheep/]
public class Account {
private int balance; public Account(int balance) {
this.balance = balance;
} synchronized public boolean withdraw(int amount){
if(balance<amount)
return false;
balance -= amount;
return true;
} synchronized public void deposit(int amount){
balance +=amount;
} }

3. synchronized关键字

上面我们已经看到synchronized的一种用法,用来修饰方法,表示进入该方法需要对Intrinsic lock加锁,离开时放锁。synchronized可以用在程序块中,显示说明对“哪个对象的Intrinsic lock加锁”,比如

 Example Source Code [http://www.cnblogs.com/tomsheep/]
synchronized public void deposit(int amount){
balance +=amount;
}
// 等价于
public void deposit(int amount){
synchronized(this){
balance +=amount;
}
}

这时,你可能就要问了,你不是说任何对象都有intrinsic lock么?而synchronized关键字又可以显示指定去锁谁,那我们是不是可以这样做:

 Example Source Code [http://www.cnblogs.com/tomsheep/]
public class Account {
private int balance;
private Object lock = new Object(); public Account(int balance) {
this.balance = balance;
} public boolean withdraw(int amount){
synchronized (lock) {
if(balance<amount)
return false;
balance -= amount;
return true;
}
} public void deposit(int amount){
synchronized (lock) {
balance +=amount;
}
}
}

不用this的内部锁,而是用另外任意一个对象的内部锁来完成完全相同的任务?没错,完全可以。不过,需要注意的是,这时候,你实际上禁止了“客户代码加锁”的行为。前几天BBS上简哥有一贴提到的bug其实就是这个,这个时候使用这份代码的客户程序如果想当然地认为Account的同步是基于其内部锁的,并且傻X兮兮地写了类似下面的代码:

 Example Source Code [http://www.cnblogs.com/tomsheep/]
	public static void main(String[] args) {
Account account =new Account(1000); //some threads modifying account through Account’s methods... synchronized (account) {
;//blabla
}
}

自认为后面的同步快对account加了锁,期间的操作不会被其余通过Account方法操作account对象的线程所干扰,那就太悲剧了。因为他们并不相干,锁住了不同的锁。

4. Java中的条件变量

正如我们前面所说,Java采取了wait/notify机制来作为intrinsic lock 相关的条件变量,表示为等待某一条件成立的条件队列——说到这里顺带插一段,条件队列必然与某个锁相关,并且语义上关联某个谓词(条件队列、锁、条件谓词就是吉祥的一家)。所以,在使用wait/notify方法时,必然是已经获得相关锁了的,在进一步说,一个推论就是“wait/notify  方法只能出现在相应的同步块中”。如果不呢?就像下面一段(notify表示的谓词是“帐户里有钱啦~”):

 Example Source Code [http://www.cnblogs.com/tomsheep/]
	public void deposit(int amount){
balance +=amount;
notify();
} //或者这样: public void deposit(int amount){
synchronized (lock) {
balance +=amount;
notify();
}
}

这两段都是错的,第一段没有在同步块里,而第二段拿到的是lock的内部锁,调用的却是this.notify(),让人遗憾。运行时他们都会抛IllegalMonitorStateException异常——唉,想前一阵我参加一次笔试的时候,有一道题就是这个,让你选所给代码会抛什么异常,我当时就傻了,想这考得也太偏了吧,现在看看,确实是很基本的概念,当初被虐是压根没有理解wait/notify机制的缘故。那怎么写是对的呢?

 Example Source Code [http://www.cnblogs.com/tomsheep/]
	public void deposit(int amount){
synchronized (lock) {
balance +=amount;
lock.notify();
}
}
//或者(取决于你采用的锁):
synchronized public void deposit(int amount){
balance +=amount;
notify();
}

5.这就够了吗?

看上去,Java的内部锁和wait/notify机制已经可以满足任何同步需求了,不是吗?em…可以这么说,但也可以说,不那么完美。有两个问题:

  • 锁不够用

有时候,我们的类里不止有一个状态,这些状态是相互独立的,如果只用同一个内部锁来维护他们全部,未免显得过于笨拙,会严重影响吞吐量。你马上会说,你刚才不是演示了用任意一个Object来做锁吗?我们多整几个Object分别加锁不就行了吗?没错,是可行的。但这样可能显得有些丑陋,而且Object来做锁本身就有语义不明确的缺点。

  • 条件变量不够用

Java用wait/notify机制实际上默认给一个内部锁绑定了一个条件队列,但是,有时候,针对一个状态(锁),我们的程序需要两个或以上的条件队列,比如,刚才的Account例子,如果某个2B银行有这样的规定“一个账户存款不得多于10000元”,这个时候,我们的存钱需要满足“余额+要存的数目不大于10000,否则等待,直到满足这个限制”,取钱需要满足“余额足够,否则等待,直到有钱为止”,这里需要两个条件队列,一个等待“存款不溢出”,一个等待“存款足够”,这时,一个默认的条件队列够用么?你可能又说,够用,我们可以模仿network里的“多路复用”,一个队列就能当多个来使,像这样:

 Example Source Code [http://www.cnblogs.com/tomsheep/]
public class Account {
public static final int BOUND = 10000;
private int balance; public Account(int balance) {
this.balance = balance;
} synchronized public boolean withdraw(int amount) throws InterruptedException{
while(balance<amount)
wait();// no money, wait
balance -= amount;
notifyAll();// not full, notify
return true;
} synchronized public void deposit(int amount) throws InterruptedException{
while(balance+amount >BOUND)
wait();//full, wait
balance +=amount;
notifyAll();// has money, notify
}
}

不是挺好吗?恩,没错,是可以。但是,仍然存在性能上的缺陷:每次都有多个线程被唤醒,而实际只有一个会运行,频繁的上下文切换和锁请求是件很废的事情。我们能不能不要notifyAll,而每次只用notify(只唤醒一个)呢?不好意思,想要“多路复用”,就必须notifyAll,否则会有丢失信号之虞(不解释了)。只有满足下面两个条件,才能使用notify:

一,只有一个条件谓词与条件队列相关,每个线程从wait返回执行相同的逻辑。

二,一进一出:一个对条件变量的通知,语义上至多只激活一个线程。

我又想插播一段:刚才写上面那段代码,IDE提示抛InterruptedException,我想提一下,这是因为wait是一个阻塞方法,几乎所有阻塞方法都会声明可能抛InterruptedException,这是和Java的interrupt机制有关的,以后我们有机会再说。

既然这么做不优雅不高效不亚克西,那如之奈何?Java提供了其他工具吗?是的。这就是传说中的java.util.concurrency包里的故事,今天也不说了,有机会在和大家讨论。

主要参考资料:

1. Wiki

2. Addison Wesley, Java Concurrency in Practice ,Brian Goetz

Java 并行 (2): Monitor的更多相关文章

  1. 探索 Java 同步机制[Monitor Object 并发模式在 Java 同步机制中的实现]

    探索 Java 同步机制[Monitor Object 并发模式在 Java 同步机制中的实现] https://www.ibm.com/developerworks/cn/java/j-lo-syn ...

  2. Java 并行与并发

    Java 并行与并发 注意两个词:并行(Concurrent) 并发(Parallel) 并行:是逻辑上同时发生,指在某一个时间内同时运行多个程序 并发:是物理上同时发生,指在某一个时间点同时运行多个 ...

  3. Java并发程序设计(二)Java并行程序基础

    Java并行程序基础 一.线程的生命周期 其中blocked和waiting的区别: 作者:赵老师链接:https://www.zhihu.com/question/27654579/answer/1 ...

  4. JAVA并行程序基础

    JAVA并行程序基础 一.有关线程你必须知道的事 进程与线程 在等待面向线程设计的计算机结构中,进程是线程的容器.我们都知道,程序是对于指令.数据及其组织形式的描述,而进程是程序的实体. 线程是轻量级 ...

  5. 杂项-Java:Druod Monitor

    ylbtech-杂项-Java:Druid Monitor 1.返回顶部 1. https://www.cnblogs.com/wanghuijie/p/druid_monitor.html 2. 2 ...

  6. JAVA并行程序基础二

    JAVA并行程序基础二 线程组 当一个系统中,如果线程较多并且功能分配比较明确,可以将相同功能的线程放入同一个线程组里. activeCount()可获得活动线程的总数,由于线程是动态的只能获取一个估 ...

  7. JAVA并行程序基础一

    JAVA并行程序基础一 线程的状态 初始线程:线程的基本操作 1. 新建线程 新建线程只需要使用new关键字创建一个线程对象,并且用start() ,线程start()之后会执行run()方法 不要直 ...

  8. Java:并行编程及同步使用方法

    知道java可以使用java.util.concurrent包下的 CountDownLatch ExecutorService Future Callable 实现并行编程,并在并行线程同步时,用起 ...

  9. JAVA并行框架:Fork/Join

    一.背景 虽然目前处理器核心数已经发展到很大数目,但是按任务并发处理并不能完全充分的利用处理器资源,因为一般的应用程序没有那么多的并发处理任务.基于这种现状,考虑把一个任务拆分成多个单元,每个单元分别 ...

随机推荐

  1. [转]十五天精通WCF——第五天 你需要了解的三个小技巧

    一: 服务是端点的集合 当你在开发wcf的时候,你或许已经注意到了一个service可以公布多个endpoint,确实是这样,在wcf中有一句很经典的话,叫做“服务是端点的集合",就 比如说 ...

  2. CF #329 C

    C题我还以为是拉格朗日插值... 其实可以想象到,必须有这样一个函数,经过某一点时,其它圆相关的函数要为0. 于是,可以构造这样的一个函数,对于x有 (x/2)*(1-abs(t-i)+abs(1-a ...

  3. OC3大回调模式使用总结(三)block回调

    OC 3大回调模式使用总结(三)block回调 block 又称 代码块,闭包等 是一个匿名的函数,它能够当做一个对象来使用,仅仅只是这个对象非常特殊,是一段代码,他能够保存你写的一段预备性质代码,待 ...

  4. Delphi7中单元文件内各个部分的执行顺序

    注:本文主要是讨论delphi程序启动时和退出时的执行顺序,期间有些知识来源于Delphi帮助,有些来自<Delphi7程序设计教程>(这本书只告诉我有initialization 和 f ...

  5. NYOJ15括号匹配

    NYOJ15括号匹配 括号匹配(二) 时间限制:1000 ms  |  内存限制:65535 KB 难度:6   描述 给你一个字符串,里面只包含"(",")" ...

  6. CodeForces - 810C(规律)

    C. Do you want a date? time limit per test 2 seconds memory limit per test 256 megabytes input stand ...

  7. PCB .NET连接MySQL与Oracle DLL文分享件 MySql.Data,Oracle.ManagedDataAccess

    虽然我们C#对SQL SERVER天然的支持,但对于C#要连接MYSQL或Oracle就不同了, 需要用到第3方组件才行,本文将2个组件连接数据库代码与DLL下载地址贴出. 一.C#连接MYSQL   ...

  8. 【BZOJ4555】【TJOI2016】【HEOI2016】求和

    题目 传送门 解法 我们可以用容斥来求第二类斯特林数 我们知道, 第二类斯特林数\(S(n, k)\)是\(n\)个元素放进\(k\)个无标号的盒子里, 不可以含有空的. 于是我们可以考虑可以含有空的 ...

  9. set()集合的概念与一般操作

    1.概念 set集合是python的一种基本数据类型,其特点为: 1.元素不重复(可以利用这条性质除去重复元素) 2.在集合中无序 3.元素可hash(int,str,bool,tuple) set集 ...

  10. css3 选择器 权重问题 (第一部分)

    其实我现在写的这些博文笔记都是我很早之前学习的时候所写的笔记,只不过之前是没有写博客的习惯,所以都是写在word文档中个人需要的时候看而已.最近刚刚开了博客,所以想将自己的笔记贴到博客. 但是现在看来 ...