一步一步掌握线程机制(五)---等待与通知机制

      在之前我们关于停止Thread的讨论中,曾经使用过设定标记done的做法,一旦done设置为true,线程就会结束,一旦为false,线程就会永远运行下去。这样做法会消耗掉许多CPU循环,是一种对内存不友好的行为。

      java中的对象不仅拥有锁,而且它们本身就可以通过调用相关方法使自己成为等待者和通知者。

     Object对象本身有两个方法:wait()和notify()。wait()会等待条件的发生,而notify()会通知正在等待的线程此条件已经发生,它们都必须从synchronized方法或块中调用。

     这种等待-通知机制的目的究竟是为何?

     等待-通知机制是一种同步机制,但它更像是一个通信机制,能够让一个线程与另一个线程在某个特定条件下进行通信。但是,该机制却没有指定特定条件是什么。

     等待-通知机制能否取代synchronized机制吗?当然不行,等待-通知机制并不会解决synchronized机制能够解决的竞争问题,实际上,这两者是相互配合使用的,而且它本身也存在竞争问题,这是需要通过synchronzied来解决的。

private boolean done = true;

public synchronized void run(){
      while(true){
            try{
                 if(done){
                       wait();
                 }else{
                       repaint();
                       wait(100);
                 }
            }catch(InterruptedException e){
                  return;
            }
      }
}

public synchronized void setDone(boolean b){
     done = b;
     if(timer == null){
          timer = new Thread(this);
          timer.start();
     }
     if(!done){
          notify();
     }
}

     这里的done已经不是volatile,因为我们不只是设定个标记值,我们还需要在设定标记的同时自动发送一个通知。所以,我们现在是通过synchronized来保护对done的访问。
     run()方法不会在done为false时自动退出,它会通过调用wait()方法让线程在这个方法中等待,直到其他线程调用notify()方法。

     这里有几个地方值得我们注意。

     首先,我们这里通过使用wait()方法而不是sleep()方法来使线程休眠,因为wait()方法需要线程持有该对象的同步锁,当wait()方法执行的时候,该锁就会被释放,而当收到通知的时候,线程需要在wait()方法返回前重新获得该锁,就好像一直都持有锁一样。这个技巧是因为在设定与发送通知以及测试与取得通知之间是存在竞争的,如果wait()和notify()在持有同步锁的同时没有被调用,是完全没有办法保证此通知会被接收到的,并且如果wait()方法在等待前没有释放掉锁,是不可能让notify()方法被调用到,因为它无法取得锁,这也是我们之所以使用wait()而不是sleep()的另一个原因。如果使用sleep()方法,此锁就永远不会被释放,setDone()方法也永远不会执行,通知也永远不会送出。

     接着就是这里我们对run()进行同步化。我们之前讨论过,对run()进行同步是非常危险的,因为run()方法是绝对不可能会完成的,也就是锁永远不会被释放,但是因为wait()本身就会释放掉锁,所以这个问题也被避免了。

     我们会有一个疑问:如果在notify()方法被调用的时候,没有线程在等待呢?

     等待-通知机制并不知道所送出通知的条件,它会假设通知在没有线程等待的时候是没有被收到的,因为这时它也只是返回且通知也被遗失掉,稍后执行wait()方法的线程就必须等待另一个通知。

     上面我们讲过,等待-通知机制本身也存在竞争问题,这真是一个讽刺:原本用来解决同步问题的机制本身竟然也存在同步问题!其实,竞争并不一定是个问题,只要它不引发问题就行。我们现在就来分析一下这里的竞争问题:

     使用wait()的线程会确认条件不存在,这通常是通过检查变量实现的,然后我们才调用wait()方法。当其他线程设立了该条件,通常也是通过设定同一个变量,才会调用notify()方法。竞争是发生在下列几种情况:

1.第一个线程测试条件并确认它需要等待;

2.第二个线程设定此条件;

3.第二个线程调用notify()方法,这并不会被收到,因为第一个线程还没有进入等待;

4.第一个线程调用wait()方法。

      这种竞争就需要同步锁来实现。我们必须取得锁以确保条件的检查和设定都是automic,也就是说检查和设定都必须处于锁的范围内。

      既然我们上面讲到,wait()方法会释放锁然后重新获取锁,那么是否会有竞争是发生在这段期间呢?理论上是会有,但系统会阻止这种情况。wait()方法与锁机制是紧密结合的,在等待的线程还没有进入准备好可以接收通知的状态前,对象的锁实际上是不会被释放的。

      我们的疑问还在继续:线程收到通知,是否就能保证条件被正确的设定呢?抱歉,答案不是。在调用wait()方法前,线程永远应该在持有同步锁时测试条件,在从wait()方法返回时,该线程永远应该重新测试条件以判断是否还需要等待,这是因为其他的线程同样也能够测试条件并判断出无需等待,然后处理由发出通知的线程所设定的有效数据。但这是在只有一个线程在等待通知,如果是多个线程在等待通知,就会发生竞争,而且这是等待-通知机制所无法解决的,因为它能解决的只是内部的竞争以防止通知的遗失。多线程等待最大的问题就是,当一个线程在其他线程收到通知后再收到通知,它无法保证这个通知是有效的,所以等待的线程必须提供选项以供检查状态,并在通知已经被处理的情形下返回到等待的状态,这也是我们为什么总是要将wait()放在循环里面的原因。

      wait()也会在它的线程被中断时提前返回,我们的程序也必须要处理该中断。

      在多线程通知中,我们如何确保正确的线程收到通知呢?答案是不行的,因为我们根本就无法保证哪一个线程能够收到通知,能够做到的方法就是所有等待的线程都会收到通知,这是通过notifyAll()实现的,但也不是真正的唤醒所有等待的线程,因为锁的问题,实质上所有的线程都会被唤醒,但是真正在执行的线程只有一个。

       之所以要这样做,可能是因为有一个以上的条件要等待,既然我们无法确保哪一个线程会被唤醒,那就干脆唤醒所有线程,然后由它们自己根据条件判断是否要执行。

       等待-通知机制可以和synchronized结合使用:

private Object doneLock = new Object();

public void run(){
     synchronized(doneLock){
           while(true){
                if(done){
                      doneLock.wait();
                }else{
                      repaint();
                      doneLock.wait(100);
                }
           }catch(InterruptedException e){
                 return;
           }
     }
}

public void setDone(boolean b){
     synchronized(doneLock){
          done = b;
          if(timer == null){
               timer = new Thread(this);
               timer.start();
          }
          if(!done){
                doneLock.notify();
          }
     }
}

     这个技巧是非常有用的,尤其是在具有许多对对象锁的竞争中,因为它能够在同一时间内让更多的线程去访问不同的方法。
     最后我们要介绍的是条件变量。

     J2SE5.0提供了Condition接口。Condition接口是绑定在Lock接口上的,就像等待-通知机制是绑定在同步锁上一样。

private Lock lock = new ReentrantLock();
private Condition  cv = lockvar.newCondition();

public void run(){
     try{
          lock.lock();
          while(true){
               try{
                   if(done){
                         cv.await();
                   }else{
                         nextCharacter();
                         cv.await(getPauseTime(), TimeUnit.MILLISECONDS);
                   }
               }catch(InterruptedException e){
                     return;
               }
          }
     }finally{
           lock.unlock();
     }
}

public void setDone(boolean b){
    try{
         lock.lock();
         done = b;
         if(!done){
               cv.signal();
         }finally{
               lock.unlock();
         }
    }
}

    上面的例子好像是在使用另一种方式来完成我们之前的等待-通知机制,实际上使用条件变量是有几个理由的:
1.条件变量在使用Lock对象时是必须的,因为Lock对象的wait()和notify()是无法运作的,因为这些方法已经在内部被用来实现Lock对象,更重要的是,持有Lock对象并不表示持有该对象的同步锁,因为Lock对象和对象所关联的同步锁是不同的。

2.Condition对象不像java的等待-通知机制,它是被创建成不同的对象,对每个Lock对象都可以创建一个以上的Condition对象,于是我们可以针对个别的线程或者一群线程进行独立的设定,也就是说,对同一个对象上所有被同步化的在等待的线程都得等待相同的条件。

      基本上,Condition接口的方法都是复制等待-通知机制,但是提供了避免被中断或者能以相对或绝对时间来指定时限的便利。

posted @ 2013-08-13 13:13  文酱  阅读(2892)  评论(1编辑  收藏  举报