正确理解线程等待和释放(wait/notify)

来源:互联网 发布:vm centos 桥接 编辑:程序博客网 时间:2024/05/30 05:05

对于初学者来说,下面这个例子是一个非常常见的错误。

Java代码  收藏代码
  1. /** 
  2.  *  线程A: 循环50次后等待并放弃锁,让线程B执行。 
  3.  */  
  4. class ThreadA extends Thread{  
  5.      //线程同步的公共数据区     
  6.     Object oa=null;  
  7.       
  8.        ThreadA(Object o){  
  9.         this.oa=o;  
  10.     }  
  11.     //线程A执行逻辑  
  12.     public void run(){  
  13.                 //线程同步区域,需要申请公共数据的锁  
  14.         synchronized(oa){  
  15.             System.out.println("ThreadA is running......");  
  16.             for(int i=0;i<100;i++){  
  17.                 System.out.println("   ThreadA value is "+i);  
  18.                 if(i==50){  
  19.                     try {  
  20.                                                //当前线程等待  
  21.                         Thread.currentThread().wait();  
  22.                     } catch (InterruptedException e) {  
  23.                         e.printStackTrace();  
  24.                     }  
  25.                 }//if(i==50)  
  26.             }//for(int i)  
  27.         }  
  28.     }  
  29. }  
  30. /** 
  31.  *  线程B:等待线程A放弃锁,然后获得锁并执行,完成后唤醒线程A 
  32.  */  
  33. class ThreadB extends Thread{  
  34.      //线程同步的公共数据区     
  35.     Object ob=null;  
  36.       
  37.     ThreadB(Object o){  
  38.         this.ob=o;  
  39.     }  
  40.     //线程B执行逻辑  
  41.     public void run(){  
  42.         //线程同步区域,需要申请公共数据的锁  
  43.                synchronized(ob){  
  44.             System.out.println("ThreadB is running......");  
  45.             for(int i=0;i<50;i++){  
  46.                 System.out.println("   ThreadB value is "+i);  
  47.             }  
  48.                       //唤醒等待的线程  
  49.             notify();  
  50.         }  
  51.     }  
  52. }  
  53. //测试  
  54. public class ThreadTest {    
  55.      public static void main(String[] args){  
  56.          Object lock=new Object(); //公共数据区  
  57.          ThreadA threada=new ThreadA(lock);  
  58.          ThreadB threadb=new ThreadB(lock);  
  59.          threada.start(); //线程A执行  
  60.          threadb.start(); //线程B执行  
  61.      }  
  62. }    

     程序很简单,就是让线程A,B交替打印。但是运行的时候会抛出两个异常:

Exception in thread "Thread-0" java.lang.IllegalMonitorStateException: current thread not owner

Exception in thread "Thread-1" java.lang.IllegalMonitorStateException: current thread not owner

 

     问题就处在ThreadA中的Thread.currentThread().wait(); 和ThreadB中的notify();上。

 

     初学者理解wait()的时候都认为是将当前线程阻塞,所以Thread.currentThread().wairt();视乎很有道理。但是不知道大家有没有发现,在JDK类库中wait()和notify()方法并不是Thread类的,而是Object()中的。我们仔细看看wait方法的JDK文档:

 

    public final void wait() throws InterruptedException

 

    在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,当前线程等待。 换句话说,此方法的行为就好像它仅执行 wait(0) 调用一样。

    当前线程必须拥有此 对象监视器 该线程发布对此监视器的所有权并等待 ,直到其他线程通过调用 notify 方法,或 notifyAll 方法通知在此对象的监视器上等待的线程醒来。 然后该线程将等到重新获得对监视器的所有权后才能继续执行。  

    对于某一个参数的版本,实现中断和虚假唤醒是可能的,而且此方法应始终在循环中使用:

                                    synchronized (obj) {
                                           while (<condition does not hold>)
                                                 obj.wait(); 
                                           // Perform action appropriate to condition
                                     }

     此方法只应由作为此对象监视器的所有者的线程来调用。

     抛出: IllegalMonitorStateException - 如果当前线程不是此对象监视器的所有者。

               InterruptedException - 如果在当前线程等待通知之前或者正在等待通知时,任何线程中断了当前线程。在抛出此异常时,当前线程的中断状态 被清除。

 

 

    看完JDK文档以后,很显然,只要把开始的程序中Thread.currentThread().wait();改成oa.wait() 。 notify();改成ob.notify()就没有问题了。

 

     也就是说,只能通过同步块obj来调用wait/notify方法 ,而不能通过想当然的线程调用这两个方法。至于为什么是这样,我有一种想法,大家可以一起讨论一下:

 

      首先,我们都知道JVM会给每一个对象都分配唯一的一把锁。这把锁是在对象中的。

      然后,当Thread-0线程获得了这把锁后,应该是在对象中的锁内记录下当前占有自己的线程号,并把自己设置为已被占用。那么当Thread-0需要放弃锁的时候,锁对象会把 Thread-0放入到锁的等待队列中 。而这一切和Thread-0是没有任何关系的。自然也轮不到Thread-0对象来调用某个方法来改变另一个对象中的锁(这一点也说不通,我自己的锁凭什么让你来改)。

      因此,也就出现用改变公共数据区对象的锁的方法是通过共数据区对象本省来调用,和线程对象是没有关系的。

 

      事实上,每一个同步锁lock下面都挂了几个线程队列,包括就绪(Ready)队列,等待(Waiting)队列等。当线程A因为得不到同步锁lock,从而进入的是lock.ReadyQueue(就绪队列),一旦同步锁不被占用,JVM将自动运行就绪队列中的线程而不需要任何notify()的操作。但是当线程A被wait()了,那么将进入lock.WaitingQuene(等待队列),同时如果占据的同步锁也会放弃。而此时如果同步锁不唤醒等待队列中的进程(lock.notify()),这些进程将永远不会得到运行的机会。

 

      就绪队列和等待队列有很大的不同,这一点要牢记。

 

      对于object的wait、notify、notifyAll的理解,可以参见《Java 虚拟机体系结构》中堆内存区的内容,就会有更加深刻的理解了。

原创粉丝点击