Java并发编程之美
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.3 线程通知与等待

Java中的Object类是所有类的父类,鉴于继承机制,Java把所有类都需要的方法放到了Object类里面,其中就包含本节要讲的通知与等待系列函数。

1.wait()函数

当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:(1)其他线程调用了该共享对象的notify()或者notifyAll()方法;(2)其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。

另外需要注意的是,如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常。

那么一个线程如何才能获取一个共享变量的监视器锁呢?

(1)执行synchronized同步代码块时,使用该共享变量作为参数。

synchronized(共享变量){
      //doSomething
  }

(2)调用该共享变量的方法,并且该方法使用了synchronized修饰。

synchronized void add(int a, int b){
      //doSomething
}

另外需要注意的是,一个线程可以从挂起状态变为可以运行状态(也就是被唤醒),即使该线程没有被其他线程调用notify()、notifyAll()方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒。

虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用wait()方法进行防范。退出循环的条件是满足了唤醒该线程的条件。

    synchronized (obj) {
            while (条件不满足){
              obj.wait();
              }
    }

如上代码是经典的调用共享变量wait()方法的实例,首先通过同步块获取obj上面的监视器锁,然后在while循环内调用obj的wait()方法。

下面从一个简单的生产者和消费者例子来加深理解。如下面代码所示,其中queue为共享变量,生产者线程在调用queue的wait()方法前,使用synchronized关键字拿到了该共享变量queue的监视器锁,所以调用wait()方法才不会抛出IllegalMonitorStateException异常。如果当前队列没有空闲容量则会调用queued的wait()方法挂起当前线程,这里使用循环就是为了避免上面说的虚假唤醒问题。假如当前线程被虚假唤醒了,但是队列还是没有空余容量,那么当前线程还是会调用wait()方法把自己挂起。

//生产线程
synchronized (queue) {
    //消费队列满,则等待队列空闲
    while (queue.size() == MAX_SIZE) {
        try {
            //挂起当前线程,并释放通过同步块获取的queue上的锁,让消费者线程可以获取该锁,然后
              获取队列里面的元素
            queue.wait();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
    //空闲则生成元素,并通知消费者线程
    queue.add(ele);
    queue.notifyAll();
    }
}
//消费者线程
synchronized (queue) {
    //消费队列为空
    while (queue.size() == 0) {
        try
            //挂起当前线程,并释放通过同步块获取的queue上的锁,让生产者线程可以获取该锁,将生
              产元素放入队列
            queue.wait();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
    //消费元素,并通知唤醒生产者线程
    queue.take();
    queue.notifyAll();
    }
}

在如上代码中假如生产者线程A首先通过synchronized获取到了queue上的锁,那么后续所有企图生产元素的线程和消费线程将会在获取该监视器锁的地方被阻塞挂起。线程A获取锁后发现当前队列已满会调用queue.wait()方法阻塞自己,然后释放获取的queue上的锁,这里考虑下为何要释放该锁?如果不释放,由于其他生产者线程和所有消费者线程都已经被阻塞挂起,而线程A也被挂起,这就处于了死锁状态。这里线程A挂起自己后释放共享变量上的锁,就是为了打破死锁必要条件之一的持有并等待原则。关于死锁后面的章节会讲。线程A释放锁后,其他生产者线程和所有消费者线程中会有一个线程获取queue上的锁进而进入同步块,这就打破了死锁状态。

另外需要注意的是,当前线程调用共享变量的wait()方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。下面来看一个例子。

    // 创建资源
    private static volatile  Object resourceA = new Object();
    private static volatile Object resourceB = new Object();
    public static void main(String[] args) throws InterruptedException {
        // 创建线程
        Thread threadA = new Thread(new Runnable() {
            public void run() {
                try {
                    // 获取resourceA共享资源的监视器锁
                    synchronized (resourceA) {
                        System.out.println("threadA get resourceA lock");
                        // 获取resourceB共享资源的监视器锁
                        synchronized (resourceB) {
                            System.out.println("threadA get resourceB lock");
                            // 线程A阻塞,并释放获取到的resourceA的锁
                            System.out.println("threadA release resourceA lock");
                            resourceA.wait();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        // 创建线程
        Thread threadB = new Thread(new Runnable() {
            public void run() {
                try {
              //休眠1s
                    Thread.sleep(1000);
                    // 获取resourceA共享资源的监视器锁
                    synchronized (resourceA) {
                        System.out.println("threadB get resourceA lock");
                        System.out.println("threadB try get resourceB lock...");
                        // 获取resourceB共享资源的监视器锁
                        synchronized (resourceB) {
                            System.out.println("threadB get resourceB lock");
                            // 线程B阻塞,并释放获取到的resourceA的锁
                            System.out.println("threadB release resourceA lock");
                            resourceA.wait();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        // 启动线程
        threadA.start();
        threadB.start();
        // 等待两个线程结束
        threadA.join();
        threadB.join();
        System.out.println("main over");
    }

输出结果如下:

如上代码中,在main函数里面启动了线程A和线程B,为了让线程A先获取到锁,这里让线程B先休眠了1s,线程A先后获取到共享变量resourceA和共享变量resourceB上的锁,然后调用了resourceA的wait()方法阻塞自己,阻塞自己后线程A释放掉获取的resourceA上的锁。

线程B休眠结束后会首先尝试获取resourceA上的锁,如果当时线程A还没有调用wait()方法释放该锁,那么线程B会被阻塞,当线程A释放了resourceA上的锁后,线程B就会获取到resourceA上的锁,然后尝试获取resourceB上的锁。由于线程A调用的是resourceA上的wait()方法,所以线程A挂起自己后并没有释放获取到的resourceB上的锁,所以线程B尝试获取resourceB上的锁时会被阻塞。

这就证明了当线程调用共享对象的wait()方法时,当前线程只会释放当前共享对象的锁,当前线程持有的其他共享对象的监视器锁并不会被释放。

最后再举一个例子进行说明。当一个线程调用共享对象的wait()方法被阻塞挂起后,如果其他线程中断了该线程,则该线程会抛出InterruptedException异常并返回。

public class WaitNotifyInterupt {
    static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        //创建线程
        Thread threadA = new Thread(new Runnable() {
            public void run() {
                try {
                    System.out.println("---begin---");
                    //阻塞当前线程
                    synchronized (obj) {
                      obj.wait();
                }
                    System.out.println("---end---");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        threadA.start();
        Thread.sleep(1000);
        System.out.println("---begin interrupt threadA---");
        threadA.interrupt();
        System.out.println("---end interrupt threadA---");
    }
}

输出如下。

在如上代码中,threadA调用共享对象obj的wait()方法后阻塞挂起了自己,然后主线程在休眠1s后中断了threadA线程,中断后threadA在obj.wait()处抛出java.lang. InterruptedException异常而返回并终止。

2.wait(long timeout)函数

该方法相比wait()方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后,没有在指定的timeout ms时间内被其他线程调用该共享变量的notify()或者notifyAll()方法唤醒,那么该函数还是会因为超时而返回。如果将timeout设置为0则和wait方法效果一样,因为在wait方法内部就是调用了wait(0)。需要注意的是,如果在调用该函数时,传递了一个负的timeout则会抛出IllegalArgumentException异常。

3.wait(long timeout, int nanos) 函数

在其内部调用的是wait(long timeout)函数,如下代码只有在nanos>0时才使参数timeout递增1。

public final void wait(long timeout, int nanos) throws InterruptedException {
      if (timeout < 0) {
          throw new IllegalArgumentException("timeout value is negative");
      }
      if (nanos < 0 || nanos > 999999) {
          throw new IllegalArgumentException(
                              "nanosecond timeout value out of range");
      }
      if (nanos > 0) {
          timeout++;
      }
      wait(timeout);
  }

4.notify() 函数

一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。

此外,被唤醒的线程不能马上从wait方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。

类似wait系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的notify()方法,否则会抛出IllegalMonitorStateException异常。

5.notifyAll() 函数

不同于在共享变量上调用notify()函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。

下面举一个例子来说明notify()和notifyAll()方法的具体含义及一些需要注意的地方,代码如下。

// 创建资源
private static volatile Object resourceA = new Object();
public static void main(String[] args) throws InterruptedException {
    // 创建线程
    Thread threadA = new Thread(new Runnable() {
        public void run() {
            // 获取resourceA共享资源的监视器锁
            synchronized (resourceA) {
                System.out.println("threadA get resourceA lock");
                try {
                    System.out.println("threadA begin wait");
                    resourceA.wait();
                    System.out.println("threadA end wait");
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    });
    // 创建线程
    Thread threadB = new Thread(new Runnable() {
        public void run() {
            synchronized (resourceA) {
                System.out.println("threadB get resourceA lock");
                try {
                    System.out.println("threadB begin wait");
                    resourceA.wait();
                    System.out.println("threadB end wait");
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    });
    // 创建线程
    Thread threadC = new Thread(new Runnable() {
        public void run() {
            synchronized (resourceA) {
                System.out.println("threadC begin notify");
                resourceA.notify();
            }
        }
    });
    // 启动线程
    threadA.start();
    threadB.start();
    Thread.sleep(1000);
    threadC.start();
    // 等待线程结束
    threadA.join();
    threadB.join();
    threadC.join();
    System.out.println("main over");
}

输出结果如下。

如上代码开启了三个线程,其中线程A和线程B分别调用了共享资源resourceA的wait()方法,线程C则调用了nofity()方法。这里启动线程C前首先调用sleep方法让主线程休眠1s,这样做的目的是让线程A和线程B全部执行到调用wait方法后再调用线程C的notify方法。这个例子试图在线程A和线程B都因调用共享资源resourceA的wait()方法而被阻塞后,让线程C再调用resourceA的notify()方法,从而唤醒线程A和线程B。但是从执行结果来看,只有一个线程A被唤醒,线程B没有被唤醒:

从输出结果可知线程调度器这次先调度了线程A占用CPU来运行,线程A首先获取resourceA上面的锁,然后调用resourceA的wait()方法挂起当前线程并释放获取到的锁,然后线程B获取到resourceA上的锁并调用resourceA的wait()方法,此时线程B也被阻塞挂起并释放了resourceA上的锁,到这里线程A和线程B都被放到了resourceA的阻塞集合里面。线程C休眠结束后在共享资源resourceA上调用了notify()方法,这会激活resourceA的阻塞集合里面的一个线程,这里激活了线程A,所以线程A调用的wait()方法返回了,线程A执行完毕。而线程B还处于阻塞状态。如果把线程C调用的notify()方法改为调用notifyAll()方法,则执行结果如下。

从输入结果可知线程A和线程B被挂起后,线程C调用notifyAll()方法会唤醒resourceA的等待集合里面的所有线程,这里线程A和线程B都会被唤醒,只是线程B先获取到resourceA上的锁,然后从wait()方法返回。线程B执行完毕后,线程A又获取了resourceA上的锁,然后从wait()方法返回。线程A执行完毕后,主线程返回,然后打印输出。

一个需要注意的地方是,在共享变量上调用notifyAll()方法只会唤醒调用这个方法前调用了wait系列函数而被放入共享变量等待集合里面的线程。如果调用notifyAll()方法后一个线程调用了该共享变量的wait()方法而被放入阻塞集合,则该线程是不会被唤醒的。尝试把主线程里面休眠1s的代码注释掉,再运行程序会有一定概率输出下面的结果。

也就是在线程B调用共享变量的wait()方法前线程C调用了共享变量的notifyAll方法,这样,只有线程A被唤醒,而线程B并没有被唤醒,还是处于阻塞状态。