当一个线程调用一个共享变量的 wait()方法时, 该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:
(1)其它线程调用了该共享对象的 notify()或者 notifyAll()方法;
(2)其他线程调用了该线程的 interrupt()方法, 该线程抛出 InterruptedException 异常返回。
线程在调用wait()方法之前必须获取共享变量的监视器锁,否则会抛出 IllegalMonitorStateException 异常。
那么一个线程如何才能获取一个共享变量的监视器锁呢?
(1)执行 synchronized 同步代码块时,使用该共享变量作为参数。
synchronized(共享变量){ //doSomething }
(2)调用该共享变量的方法,并且该方法使用了 synchronized 修饰。
synchronized void add(int a, int b) { //doSomething }
另外需要注意的是,一个线程可以从挂起状态变为可以运行状态( 也就是被唤醒), 即使该线程没有被其他线程调用 notify()、 notifyAll()方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒。
下面从一个简单的生产者和消费者例子来加深理解。 如下面代码所示,其中 queue 为共享 变量,生产者线程在调用 queue 的 wait() 方法前,使用 synchronized 关键宇拿到了该共享变量 queue 的监视器锁,所以调用 waitO 方法才不会抛出 lliega!MonitorStateException 异常。如果当前 队列没有空闲容量则会调用 queued 的 wait() 方法挂起当前线程,这里使用循环就是为了 避免上面说的虚假唤醒问题。假如当前线程被虚假唤醒了,但是队列还是没有空余容量, 那么当前线程还是会调用 wait()方法把 自己挂起。
//生产线程 synchronized (queue) { //消费队列满,则等待队列空闲 while (queue.size () ==MAX SIZE) { try { //挂起当前线程,并释放通过同步块获取的queue上的锁,让消费者线程可以获取该锁,然后获取队列里面的元素 queue .wait() ; } catch (Exception ex) { ex .prinntStackTrace( ) ; } } //如果队列空闲则生成元素,并通知消费者线程 queue .add(ele) ; queue.notifyAll (); } //消费者线程 synchronized (queue) { //消费队列为空 while (queue.size () == 0) { try { //挂起当前线程,并释放通过同步块获取的queue上的锁,让生产者线程可以获取该锁,将生产元素放入队列 queue.wait(); } catch (Exception ex) { ex .printStackTrace() ; } } //消费元素,并通知唤醒生产者线程 queue.take( ); queue .notifyAll (); } wait(long timeout)函数
该方法相比 wait()方法多了一个超时参数,如果一个线程调用共享对象的该方法挂起后, 没有在指定的 timeout 时间内被其它线程调用该共享变量的 notify()或者 notifyAll()方法唤醒,那么该函数还是会因为超时而返回。如果将 timeout 设 置为 0 则和 wait 方法效果一样,因为在 wait 方法内部就是调用了 wait(0)。
notify()函数一个线程调用共享对象的 notify()方法后,会随机的唤醒一个在该共享变量上调用 wait系列方法后被挂起的线程。
此外,被唤醒的线程不能马上从 wait()方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁, 只有该线程竞争到了共享变量的监视器锁后才可以继续执行。
只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的 notify()方法,否则会抛出 IllegalMonitorStateException 异常。
notifyAll()函数notify All()方法则会唤醒所有在该共享变量上由于调用 wait系列方法而被挂起的线程。
join() 方法如遇到一种应用场景,比如需要等待多个线程全部处理完毕之后,程序才能继续执行就可以使用join方法,这个方法是Thread类提供的。
sleep()方法Thread类中有一个静态的 sleep方法,当一个执行中的线程调用了Thread的 sleep 方 法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与 CPU 的调度,但是该线程仍然拥有监视器锁。 指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与 CPU 的调度,获取到 CPU 资源后就可以继 续运行了。
yield()方法当一个线程调用 yield 方法时,该会让出 CPU 使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出 CPU 的那个线程来获取 CPU 执行权。 改方法实际中不常用。在调试或者测试时这个方法或许可以帮助复现由于并发竞 争条件导致的问题。
sleep 与 yield 方法的区别在于,当线程调用sleep方法时调用线程会被阻塞挂 起指定的时间,在这期间线程调度器不会去调度该线程。 而调用 yield 方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。
综合示例代码public class ThreadDemo { private static volatile Object resourceObj = new Object();//创建资源对象 public static void main(String[] args) throws InterruptedException { //创建线程 1 Thread thread1 =new Thread(new Runnable() { public void run(){ //获取resourceObj共享资源的监视器锁 synchronized(resourceObj){ System.out.println("thread1 获取resourceObj的监视器锁"); try { System.out.println("thread1 开始 wait"); resourceObj.wait(); System.out .println ("thread1 结束 wait"); }catch (InterruptedException e) { e.printStackTrace (); } } } }); //创建线程 2 Thread thread2 =new Thread(new Runnable() { public void run(){ //获取resourceObj共享资源的监视器锁 synchronized(resourceObj){ System.out.println("thread2 获取resourceObj的监视器锁"); try { System.out.println("thread2 开始 wait"); resourceObj.wait(); System.out .println ("thread2 结束 wait"); }catch (InterruptedException e) { e.printStackTrace (); } } } }); //创建线程 3 Thread thread3 =new Thread(new Runnable() { public void run(){ //获取resourceObj共享资源的监视器锁 synchronized(resourceObj){ System.out.println("thread3 开始 notify"); resourceObj.notify(); } } }); thread1.start (); thread2.start (); Thread.sleep(1000); thread3.start () ; //等待线程结束 thread1.join() ; thread2.join() ; thread3.join() ; System.out.println("程序运行结束"); } }
输入结果如下图
如上代码开启了三个线程,其中线程 1和线程2分别调用了共享资源 resourceObj的 wai()方法,线程3则调用了 nofity()方法。 这里启动线程3前首先调用 sleep 方法让主线 程休眠1秒,这样做的目的是让线程1和线程2全部执行到调用 wait 方法后再调用线程 3的 notify 方法。执行结果来看, 只有一个线程 A 被唤醒,线程 B 没有被唤醒,仍无处理阻塞状态。程序没有运行结束。
如果把线程3调用的 notify()方法改为调用notifyAll() 方法,则执行结果如下。
从结果可以看出这里线程1和线程2都会被唤醒,然后执行完成后,程序运行结束。