Java监视器支持两种线程:互斥和协作。
前面我们介绍了采用对象锁和重入锁来实现的互斥。这一篇中,我们来看一看线程的协作。 举个例子:有一家汉堡店举办吃汉堡比赛,决赛时有3个顾客来吃,3个厨师来做,一个服务员负责协调汉堡的数量。为了避免浪费,制作好的汉堡被放进一个能装有10个汉堡的长条状容器中,按照先进先出的原则取汉堡。如果容器被装满,则厨师停止做汉堡,如果顾客发现容器内的汉堡吃完了,就可以拍响容器上的闹铃,提醒厨师再做几个汉堡出来。此时服务员过来安抚顾客,让他等待。而一旦厨师的汉堡做出来,就会让服务员通知顾客,汉堡做好了,让顾客继续过来取汉堡。 这里,顾客其实就是我们所说的消费者,而厨师就是生产者。容器是决定厨师行为的监视器,而服务员则负责监视顾客的行为。 在JVM中,此种监视器被称为等待并唤醒监视器。 转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ 在这种监视器中,一个已经持有该监视器的线程,可以通过调用监视对象的wait方法,暂停自身的执行,并释放监视器,自己进入一个等待区,直到监视器内的其他线程调用了监视对象的notify方法。当一个线程调用唤醒命令以后,它会持续持有监视器,直到它主动释放监视器。而这之后,等待线程会苏醒,其中的一个会重新获得监视器,判断条件状态,以便决定是否继续进入等待状态或者执行监视区域,或者退出。 请看下面的代码: 1. public class NotifyTest { 2. private String flag = "true"; 3. 4. class NotifyThread extends Thread{ 5. public NotifyThread(String name) { 6. super(name); 7. } 8. public void run() { 9. try { 10. sleep(3000);//推迟3秒钟通知 11. } catch (InterruptedException e) { 12. e.printStackTrace(); 13. } 14. 15. flag = "false"; 16. flag.notify(); 17. } 18. }; 19. 20. class WaitThread extends Thread { 21. public WaitThread(String name) { 22. super(name); 23. } 24. 25. public void run() { 26. 27. while (flag!="false") { 28. System.out.println(getName() + " begin waiting!"); 29. long waitTime = System.currentTimeMillis(); 30. try { 31. flag.wait(); 32. } catch (InterruptedException e) { 33. e.printStackTrace(); 34. } 35. waitTime = System.currentTimeMillis() - waitTime; 36. System.out.println("wait time :"+waitTime); 37. } 38. System.out.println(getName() + " end waiting!"); 39. 40. } 41. } 42. 43. public static void main(String[] args) throws InterruptedException { 44. System.out.println("Main Thread Run!"); 45. NotifyTest test = new NotifyTest(); 46. NotifyThread notifyThread =test.new NotifyThread("notify01"); 47. WaitThread waitThread01 = test.new WaitThread("waiter01"); 48. WaitThread waitThread02 = test.new WaitThread("waiter02"); 49. WaitThread waitThread03 = test.new WaitThread("waiter03"); 50. notifyThread.start(); 51. waitThread01.start(); 52. waitThread02.start(); 53. waitThread03.start(); 54. } 55. 56. } 转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ 这段代码启动了三个简单的wait线程,当他们处于等待状态以后,试图由一个notify线程来唤醒。 转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ 运行这段程序,你会发现,满屏的java.lang.IllegalMonitorStateException,根本不是你想要的结果。 转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ 请注意以下几个事实: 1. 任何一个时刻,对象的控制权(monitor)只能被一个线程拥有。 2. 无论是执行对象的wait、notify还是notifyAll方法,必须保证当前运行的线程取得了该对象的控制权(monitor)。 3. 如果在没有控制权的线程里执行对象的以上三种方法,就会报java.lang.IllegalMonitorStateException异常。 4. JVM基于多线程,默认情况下不能保证运行时线程的时序性。 也就是说,当线程在调用某个对象的wait或者notify方法的时候,要先取得该对象的控制权,换句话说,就是进入这个对象的监视器。 转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ 通过前面对同步的讨论,我们知道,要让一个线程进入某个对象的监视器,通常有三种方法: 转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ 1: 执行对象的某个同步实例方法 2: 执行对象对应的同步静态方法 3: 执行对该对象加同步锁的同步块 显然,在上面的例程中,我们用第三种方法比较合适。 于是我们将上面的wait和notify方法调用包在同步块中。 1. synchronized (flag) { 2. flag = "false"; 3. flag.notify(); 4. } 转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ 1. synchronized (flag) { 2. while (flag!="false") { 3. System.out.println(getName() + " begin waiting!"); 4. long waitTime = System.currentTimeMillis(); 5. try { 6. flag.wait(); 7. } catch (InterruptedException e) { 8. e.printStackTrace(); 9. } 10. waitTime = System.currentTimeMillis() - waitTime; 11. System.out.println("wait time :"+waitTime); 12. } 13. System.out.println(getName() + " end waiting!"); 14. } 但是,运行这个程序,我们发现事与愿违。那个非法监视器异常又出现了。。。 转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ 我们注意到,针对flag的同步块中,我们实际上已经更改了flag对对象的引用: flag="false"; 转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ 显然,这样一来,同步块也无能为力了,因为我们根本不是针对唯一的一个对象在进行同步。 我们不妨将flag封装到JavaBean或者数组中去,这样用JavaBean对象或者数组对象进行同步,就可以达到既能修改里面参数又不耽误同步的目的。 1. private String flag[] = {"true"}; 1. synchronized (flag) { 2. flag[0] = "false"; 3. flag.notify(); 4. } 1. synchronized (flag) { 2. flag[0] = "false"; 3. flag.notify(); 4. }synchronized (flag) { 5. while (flag[0]!="false") { 6. System.out.println(getName() + " begin waiting!"); 7. long waitTime = System.currentTimeMillis(); 8. try { 9. flag.wait(); 10. 11. } catch (InterruptedException e) { 12. e.printStackTrace(); 13. } 运行这个程序,看不到异常了。但是仔细观察结果,貌似只有一个线程被唤醒。利用jconsole等工具查看线程状态,发现的确还是有两个线程被阻塞的。这是为啥呢? 程序中使用了flag.notify()方法。只能是随机的唤醒一个线程。我们可以改用flag.notifyAll()方法。这样,所有被阻塞的线程都会被唤醒了。 最终代码请读者自己修改,这里不再赘述。 好了,亲爱的读者们,让我们回到开篇提到的汉堡店大赛问题当中去,来看一看厨师、服务生和顾客是怎么协作进行这个比赛的。 首先我们构造故事中的三个次要对象:汉堡包、存放汉堡包的容器、服务生 public class Waiter {//服务生,这是个配角,不需要属性。 } class Hamberg { //汉堡包 private int id;//汉堡编号 private String cookerid;//厨师编号 public Hamberg(int id, String cookerid){ this.id = id; this.cookerid = cookerid; System.out.println(this.toString()+"was made!"); } @Override public String toString() { return "#"+id+" by "+cookerid; } } class HambergFifo { //汉堡包容器 List<Hamberg> hambergs = new ArrayList<Hamberg>();//借助ArrayList来存放汉堡包 int maxSize = 10;//指定容器容量 //放入汉堡 public <T extends Hamberg> void push(T t) { hambergs.add(t); } //取出汉堡 public Hamberg pop() { Hamberg h = hambergs.get(0); hambergs.remove(0); return h; } //判断容器是否为空 public synchronized boolean isEmpty() { return hambergs.isEmpty(); } //判断容器内汉堡的个数 public synchronized int size() { return hambergs.size(); } //返回容器的最大容量 public synchronized int getMaxSize() { return this.maxSize; } //判断容器是否已满,未满为真 public synchronized boolean isNotFull(){ return hambergs.size() < this.maxSize; } } 接下来我们构造厨师对象: class Cooker implements Runnable { //厨师要面对容器 HambergFifo pool; //还要面对服务生 Waiter waiter; public Cooker(Waiter waiter, HambergFifo hambergStack) { this.pool = hambergStack; this.waiter = waiter; } //制造汉堡 public void makeHamberg() { //制造的个数 int madeCount = 0; //因为容器满,被迫等待的次数 int fullFiredCount = 0; try { while (true) { //制作汉堡前的准备工作 Thread.sleep(1000); if (pool.isNotFull()) { synchronized (waiter) { //容器未满,制作汉堡,并放入容器。 pool.push(new Hamberg(++madeCount,Thread.currentThread().getName())); //说出容器内汉堡数量 System.out.println(Thread.currentThread().getName() + ": There are " + pool.size() + " Hambergs in all"); //让服务生通知顾客,有汉堡可以吃了 waiter.notifyAll(); System.out.println("### Cooker: waiter.notifyAll() :"+ " Hi! Customers, we got some new Hambergs!"); } } else { synchronized (pool) { if (fullFiredCount++ < 10) { //发现容器满了,停止做汉堡的尝试。 System.out.println(Thread.currentThread().getName() + ": Hamberg Pool is Full, Stop making hamberg"); System.out.println("### Cooker: pool.wait()"); //汉堡容器的状况使厨师等待 pool.wait(); } else { return; } } } //做完汉堡要进行收尾工作,为下一次的制作做准备。 Thread.sleep(1000); } } catch (Exception e) { madeCount--; e.printStackTrace(); } } public void run() { makeHamberg(); } } 接下来,我们构造顾客对象: class Customer implements Runnable { //顾客要面对服务生 Waiter waiter; //也要面对汉堡包容器 HambergFifo pool; //想要记下自己吃了多少汉堡 int ateCount = 0; //吃每个汉堡的时间不尽相同 long sleeptime; //用于产生随机数 Random r = new Random(); public Customer(Waiter waiter, HambergFifo pool) { this.waiter = waiter; this.pool = pool; } public void run() { while (true) { try { //取汉堡 getHamberg(); //吃汉堡 eatHamberg(); } catch (Exception e) { synchronized (waiter) { System.out.println(e.getMessage()); //若取不到汉堡,要和服务生打交道 try { System.out.println("### Customer: waiter.wait():"+ " Sorry, Sir, there is no hambergs left, please wait!"); System.out.println(Thread.currentThread().getName() + ": OK, Waiting for new hambergs"); //服务生安抚顾客,让他等待。 waiter.wait(); continue; } catch (InterruptedException ex) { ex.printStackTrace(); } } } } } private void eatHamberg() { try { //吃每个汉堡的时间不等 sleeptime = Math.abs(r.nextInt(3000)) * 5; System.out.println(Thread.currentThread().getName() + ": I'm eating the hamberg for " + sleeptime + " milliseconds"); Thread.sleep(sleeptime); } catch (Exception e) { e.printStackTrace(); } } private void getHamberg() throws Exception { Hamberg hamberg = null; synchronized (pool) { try { //在容器内取汉堡 hamberg = pool.pop(); ateCount++; System.out.println(Thread.currentThread().getName() + ": I Got " + ateCount + " Hamberg " + hamberg); System.out.println(Thread.currentThread().getName() + ": There are still " + pool.size() + " hambergs left"); } catch (Exception e) { pool.notifyAll(); System.out.println("### Customer: pool.notifyAll()"); throw new Exception(Thread.currentThread().getName() + ": OH MY GOD!!!! No hambergs left, Waiter![Ring the bell besides the hamberg pool]"); } } } } 转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ 最后,我们构造汉堡店,让这个故事发生: 转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ public class HambergShop { Waiter waiter = new Waiter(); HambergFifo hambergPool = new HambergFifo(); Customer c1 = new Customer(waiter, hambergPool); Customer c2 = new Customer(waiter, hambergPool); Customer c3 = new Customer(waiter, hambergPool); Cooker cooker = new Cooker(waiter, hambergPool); public static void main(String[] args) { HambergShop hambergShop = new HambergShop(); Thread t1 = new Thread(hambergShop.c1, "Customer 1"); Thread t2 = new Thread(hambergShop.c2, "Customer 2"); Thread t3 = new Thread(hambergShop.c3, "Customer 3"); Thread t4 = new Thread(hambergShop.cooker, "Cooker 1"); Thread t5 = new Thread(hambergShop.cooker, "Cooker 2"); Thread t6 = new Thread(hambergShop.cooker, "Cooker 3"); t4.start(); t5.start(); t6.start(); try { Thread.sleep(10000); } catch (Exception e) { } t1.start(); t2.start(); t3.start(); } } 运行这个程序吧,然后你会看到我们汉堡店的比赛进行的很好,只是不知道那些顾客是不是会被撑到。。。 转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ 读到这里,有的读者可能会想到前面介绍的重入锁ReentrantLock。 有的读者会问:如果我用ReentrantLock来代替上面这些例程当中的 synchronized块,是不是也可以呢?感兴趣的读者不妨一试。 转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ 但是在这里,我想提前给出结论,就是, 如果用ReentrantLock的lock()和unlock()方法代替上面的synchronized块,那么上面这些程序还是要抛出java.lang.IllegalMonitorStateException异常的,不仅如此,你甚至还会看到线程死锁。原因就是当某个线程调用第三方对象的wait或者notify方法的时候,并没有进入第三方对象的监视器,于是抛出了异常信息。但此时,程序流程如果没有用finally来处理unlock方法,那么你的线程已经被lock方法上锁,并且无法解锁。程序在java.util.concurrent框架的语义级别死锁了,你用JConsole这种工具来检测JVM死锁,还检测不出来。 转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ 正确的做法就是,只使用ReentrantLock,而不使用wait或者notify方法。因为ReentrantLock已经对这种互斥和协作进行了概括。所以,根据你程序的需要,请单独采用重入锁或者synchronized一种同步机制,最好不要混用。 转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ 好了,我们现在明白:转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ 1. 线程的等待或者唤醒,并不是让线程调用自己的wait或者notify方法,而是通过调用线程共享对象的wait或者notify方法来实现。 2. 线程要调用某个对象的wait或者notify方法,必须先取得该对象的监视器。 3. 线程的协作必须以线程的互斥为前提,这种协作实际上是一种互斥下的协作。 转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ |
|