Java多线程笔记三(线程通信wait/notify/notifyAll/sleep/yield/join)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/login_sonata/article/details/78151114

一,wait()、notify()、notifyAll()

1,信号量

线程间通信的一个最简单方式是在共享对象的变量里设置信号量。线程A在一个同步块里设置boolean型成员变量hasDataToProcess为true,线程B也在同步块里读取hasDataToProcess这个成员变量。下面的例子使用了一个持有信号的对象,并提供了set和check方法:

public class MySignal{
  protected boolean hasDataToProcess = false;
  public synchronized boolean hasDataToProcess(){
    return this.hasDataToProcess;
  }
  public synchronized void setHasDataToProcess(boolean hasData){
    this.hasDataToProcess = hasData;
  }
}

线程A和B必须获得指向一个MySignal共享实例的引用,以便进行通信。如果它们持有的引用指向不同的MySingal实例,那么彼此将不能检测到对方的信号。

下边看一个例子,准备处理数据的线程B正在等待数据变为可用。换句话说,它在等待线程A的一个信号,这个信号使hasDataToProcess()返回true。线程B运行在一个循环里,以等待这个信号:

protected MySignal sharedSignal = ...
...
while(!sharedSignal.hasDataToProcess()){
  //do nothing... busy waiting
}

这种情况叫做忙等待,它没有对运行等待线程的CPU进行有效的利用,除非平均等待时间非常短,否则,让等待线程进入睡眠或者非运行状态更为明智,直到它接收到它等待的信号。

2,等待与唤醒

Java有一个内建的等待机制来允许线程在等待信号的时候变为非运行状态。java.lang.Object 类定义了三个方法,wait()、notify()和notifyAll()来实现这个等待机制。
一个线程一旦调用了任意对象的wait()方法,就会变为waiting状态,直到另一个线程调用了同一个对象的notify()方法(或者设置一个时间参数,超时后会自动唤醒)。以下是MySingal的修改版本——使用了wait()和notify()的MyWaitNotify:

public class MonitorObject{}

public class MyWaitNotify{
  MonitorObject myMonitorObject = new MonitorObject();
  public void doWait(){
    synchronized(myMonitorObject){
      try{
        myMonitorObject.wait();
      } catch(InterruptedException e){...}
    }
  }
  public void doNotify(){
    synchronized(myMonitorObject){
      myMonitorObject.notify();
    }
  }
}

调用doWait()使线程进入等待状态,而唤醒等待线程将调用doNotify()。

如你所见,为了调用wait()或者notify(),线程必须先获得那个对象的锁。也就是说,线程必须在同步块里调用wait()或者notify()。这是强制性的!一个线程如果没有持有对象锁,将不能调用wait(),notify()或者notifyAll()。当你调用它们的时候,JVM首先要检查下当前线程是否是锁的拥有者,不是则抛出IllegalMonitorStateExcept异常。

注意一个问题:调用wait()和notify()都在同步块中,且有相同的监视器对象,那么对象调用一个方法时不会阻塞别的方法吗?答案是不会。一旦线程调用了wait()方法,它就释放了所持有的监视器对象上的锁,进入waiting状态直到其他线程调用这个对象上的notify()方法或者超时。

notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法。区别在于:
notify会随机唤醒一个wait状态线程,并使它获得该对象上的锁,而不惊动其它线程,此时对于其它线程来说,它们等待的是notify信号并一直等待下去。
notifyAll唤醒所有等待的线程,使它们的状态从等待notify变成等待锁,一旦该对象被解锁,他们就会去竞争锁,最终只有一个线程能获得对象锁并执行代码,其它的线程依次执行。

3,丢失的信号与假唤醒

当notify()和notifyAll()被调用时,有可能没有线程处于等待状态。也就是说,如果一个线程先于被通知线程调用wait()前调用了notify(),等待的线程将错过这个信号。这可能使等待线程永远在等待,不再醒来。
为了避免信号丢失, 用一个变量来保存是否被通知过。在notify前,设置自己已经被通知过。在调用wait()前会检查这个变量(没被通知的话执行wait),在wait后设置自己没有被通知过,需要等待通知。

由于莫名其妙的原因,线程有可能在没有调用过notify()和notifyAll()的情况下醒来。这就是所谓的假唤醒(spurious wakeups)。
为了防止假唤醒,保存信号的成员变量将在一个while循环里接受检查,而不是在if表达式里。这样的一个while循环类似自旋锁(如果长时间不调用doNotify方法,doWait方法会一直自旋,CPU会消耗太大)。被唤醒的线程会自旋直到自旋锁(while循环)里的条件变为false。以下代码展示了这点:

public class MyWaitNotify3{

  MonitorObject myMonitorObject = new MonitorObject();
  boolean wasSignalled = false;

  public void doWait(){
    synchronized(myMonitorObject){
      while(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}

二,sleep()、yield()、join()

1,sleep()方法的作用是让当前线程暂停指定的时间(毫秒)。需要注意的是其与wait方法的区别。最简单的区别是,wait方法依赖于同步,而sleep方法可以直接调用。而更深层次的区别在于sleep方法只是暂时让出CPU的执行权,并不释放锁。而wait方法则需要释放锁。
注意:sleep()是一个静态方法。这意味着只对当前线程有效,一个常见的错误是调用t.sleep(),这里的t是一个不同于当前线程的线程。即便是执行t.sleep (),也是当前线程进入睡眠,而不是t线程。Thread类的sleep()和yield()方法都是静态的,它们将在当前正在执行的线程上运行,所以在其他处于等待状态的线程上调用这些方法是没有意义的。

2,yield()方法的作用是暂停当前线程,以便其他线程有机会执行。yield不能指定暂停的时间,并且也不能保证当前线程马上停止(当前线程让出后,可能又竞争到了锁,然后继续执行)。

3,join()方法的作用是父线程等待子线程执行完成后再执行,换句话说就是将并发的线程合并为顺序执行的线程。如果在当前main线程中执行test.join(),那么main会被阻塞,直到test完成后(类似插队)。
join方法是通过wait方法实现的,如果join的线程还在执行,则将当前线程阻塞起来,直到join的线程执行完成,当前线程才能执行。不过有一点需要注意,这里的join只调用了wait方法,却没有对应的notify方法,原因是Thread的start方法中做了相应的处理,所以当join的线程执行完成以后,会自动唤醒主线程继续往下执行。

sleep()、yield()、join()三个方法作为线程通信的操作,它们是位于Thread类中。但是,同样实现线程通信的wait()、notify()、notifyAll()方法却不是位于Thread类中,而是位于Object类中?
因为这三个方法要操作锁,而锁是所有对象的一部分,这样Java的每一个类都有用于线程间通信的基本方法。我们只能在同步控制方法或同步控制块中调用wait()、notify()、notifyAll()方法,不用考虑这个类是继承了Thread类还是实现了Runnable接口。而sleep()方法可以在非同步控制方法中调用,因为它不用操作锁。
实际上,在非同步控制方法中调用wait()、notify()、notifyAll()方法是可以通过编译的,但运行时会报IllegalMonitorStateExcept异常。



参考自:并发编程网-线程通信
Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)

猜你喜欢

转载自blog.csdn.net/login_sonata/article/details/78151114