Java多线程同步问题的探究(四)

四、协作,互斥下的协作——Java多线程协作(wait、notify、notifyAll)

Java监视器支持两种线程:互斥和协作。

前面我们介绍了采用对象锁和重入锁来实现的互斥。这一篇中,我们来看一看线程的协作。

举个例子:有一家汉堡店举办吃汉堡比赛,决赛时有3个顾客来吃,3个厨师来做,一个服务员负责协调汉堡的数量。为了避免浪费,制 作好的汉堡被放进一个能装有10个汉堡的长条状容器中,按照先进先出的原则取汉堡。如果容器被装满,则厨师停止做汉堡,如果顾客发 现容器内的汉堡吃完了,就可以拍响容器上的闹铃,提醒厨师再做几个汉堡出来。此时服务员过来安抚顾客,让他等待。而一旦厨师的汉 堡做出来,就会让服务员通知顾客,汉堡做好了,让顾客继续过来取汉堡。

这里,顾客其实就是我们所说的消费者,而厨师就是生产者。容器是决定厨师行为的监视器,而服务员则负责监视顾客的行为。

在JVM中,此种监视器被称为等待并唤醒监视器。

在这种监视器中,一个已经持有该监视器的线程,可以通过调用监视对象的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.}

这段代码启动了三个简单的wait线程,当他们处于等待状态以后,试图由一个notify线程来唤醒。

运行这段程序,你会发现,满屏的java.lang.IllegalMoniTorStateException,根本不是你想要的结果。

请注意以下几个事实:

1.任何一个时刻,对象的控制权(moniTor)只能被一个线程拥有。

2.无论是执行对象的wait、notify还是notifyAll方法,必须保证当前运行的线程取得了该对象的控制权(moniTor)。

3.如果在没有控制权的线程里执行对象的以上三种方法,就会报java.lang.IllegalMoniTorStateException异常。

4.JVM基于多线程,默认情况下不能保证运行时线程的时序性。

也就是说,当线程在调用某个对象的wait或者notify方法的时候,要先取得该对象的控制权,换句话说,就是进入这个对象的监视器。

通过前面对同步的讨论,我们知道,要让一个线程进入某个对象的监视器,通常有三种方法:

1: 执行对象的某个同步实例方法

2: 执行对象对应的同步静态方法

3: 执行对该对象加同步锁的同步块

显然,在上面的例程中,我们用第三种方法比较合适。

于是我们将上面的wait和notify方法调用包在同步块中。

1.synchronized (flag) {2.flag = "false";3.flag.notify();4.}

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.}

但是,运行这个程序,我们发现事与愿违。那个非法监视器异常又出现了。。。

我们注意到,针对flag的同步块中,我们实际上已经更改了flag对对象的引用: flag=”false”;

显然,这样一来,同步块也无能为力了,因为我们根本不是针对唯一的一个对象在进行同步。

我们不妨将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 hambergs = new ArrayList();//借助ArrayList来存放汉堡包         int maxSize = 10;//指定容器容量         //放入汉堡         public  void push(T t) {             hambergs.add(t);         }         //取出汉堡         public Hamberg pop() {             Hamberg h = hambergs.get(0);             hambergs.remove(0);             return h;         }         //判断容器是否为空         public boolean isEmpty() {             return hambergs.isEmpty();         }         //判断容器内汉堡的个数         public int size() {             return hambergs.size();         }         //返回容器的最大容量         public int getMaxSize() {             return 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.size() < pool.getMaxSize()) {                         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]");                 }             }         }     }

最后,我们构造汉堡店,让这个故事发生:

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();     }}

运行这个程序吧,然后你会看到我们汉堡店的比赛进行的很好,只是不

知道那些顾客是不是会被撑到。。。

读到这里,有的读者可能会想到前面介绍的重入锁ReentrantLock。

有的读者会问:如果我用ReentrantLock来代替上面这些例程当中的 synchronized块,是不是也可以呢?感兴趣的读者不妨一试。

但是在这里,我想提前给出结论,就是,

如果用ReentrantLock的lock()和unlock()方法代替上面的synchronized块,那么上面这些程序还是要抛出 java.lang.IllegalMoniTorStateException异常的,不仅如此,你甚至还会看到线程死锁。原因就是当某个线程调用第三方对象的wait或 者notify方法的时候,并没有进入第三方对象的监视器,于是抛出了异常信息。但此时,程序流程如果没有用finally来处理 unlock方法 ,那么你的线程已经被lock方法上锁,并且无法解锁。程序在java.util.concurrent框架的语义级别死锁了,你用 JConsole这种工具来检 测JVM死锁,还检测不出来。

正确的做法就是,只使用ReentrantLock,而不使用wait或者notify方法。因为ReentrantLock已经对这种互斥和协作进行了概括。所以 ,根据你程序的需要,请单独采用重入锁或者synchronized一种同步机制,最好不要混用。

好了,我们现在明白:

1.线程的等待或者唤醒,并不是让线程调用自己的wait或者notify方法,而是通过调用线程共享对象的wait或者notify方法来实现。

2.线程要调用某个对象的wait或者notify方法,必须先取得该对象的监视器。

3.线程的协作必须以线程的互斥为前提,这种协作实际上是一种互斥下的协作。

下一讲当中,我们来看看如何实实在在的解决线程之间抢占共享资源的问题。敬请期待!

当你能爱的时候就不要放弃爱

Java多线程同步问题的探究(四)

相关文章:

你感兴趣的文章:

标签云: