Java 中 wait 和 sleep 的区别 -- ISS (Ideas Should Spread)
本文是笔者 Java 学习笔记之一,旨在总结个人学习 Java 过程中的心得体会,现将该笔记发表,希望能够帮助在这方面有疑惑的同行解决一点疑惑,我的目的也就达到了。欢迎分享和转载,转载请注明出处,谢谢合作。由于笔者水平有限,文中难免有所错误,希望读者朋友不吝赐教,欢迎斧正。(可在文末评论区说明或索要联系方式进一步沟通。)
所属对象不同
Java 是一门面向对象的语言,Java 中所有东西都是对象,对于一个方法的定位应该是 (package).(object).(method)
的方法,因此对于这两个方法来说第一个区别就是他们所属的对象不同,wait
方法是属于 java.lang.Object
类,由于 Java 中 java.lang.Object
是所有类的根类,因此 Java 中所有的类都具有 wait
方法,而 sleep
方法单独属于线程类 (java.lang.Thread
)。需要注意的是,由于 Object
类也是 Thread
类的根类(尽管没有显示写出 extends Object
),所以 Thread
类也具有 wait
方法,但是在调用 Thread
对象实例的时候,此时是把 Thread
的对象实例当成普通的对象实例来用(而且这种情况很少见),与线程无关。
使用效果不同
在一个对象上调用
wait
方法时,当前执行的线程会释放对该对象拥有的锁(或监视器 monitor),并在合适的时候退出该锁或监视器的范围,比如:public void foo(){ synchronized (bar){ // 其它代码 bar.wait(); System.out.println("hello world"); // 其他代码 } }
在调用完
bar.wait();
的时候当前线程会释放对bar
对象的锁,但是要离开该对象的监视器还要等synchronized
代码块执行完毕,也就是要等synchronized
代码块执行完毕后其它线程才能够有机会获得bar
的锁。在一个线程中调用
sleep(long millis)
方法会导致该线程进入休眠状态,即该线程会停止运行millis
毫秒的时间,但并不代表millis
毫秒后该线程马上会运行,超时后该线程重新加入到等待队列等待虚拟机调度。同一个线程中调用这两个方法导致线程进入的状态不同,具体如下:
注意
wait()
方法有重载方法wait(long millis)
在调用
wait()
方法不带超时参数时,调用线程会进入WAITING
状态(定义在Thread
类内部的枚举类型),而调用wait(long millis)
重载方法时,调用线程会进入TIMED_WAITING
状态;而调用sleep(long millis)
则只有进入TIMED_WAITING
的状态因为sleep
方法的另一个重载方法也是指定超时时间,只不过提高了精度而已。
使用前提不同
另一个不同之处在于 wait
和 sleep
的使用场景不同:
如上所述:一个线程对一个对象调用
wait
方法会释放对该对象持有的监视器,在唤醒的时候会重新获取该对象的监视器。那么前提就是对该对象调用wait
方法的调用线程当前持有该对象的监视器(锁),否则调用该对象的wait
方法会抛出java.lang.IllegalMonitorStateException
异常,也就是说在没有获得监视器的情况下却要释放监视器,属于监视器状态异常。因此对于wait()
方法的使用必须在先获取其对象监视器的前提下进行,如:public void foo(){ sychronized(bar){ // 对 bar 做一些事 bar.wait(); } }
一个线程调用
sleep
方法是将自己的运行权交出,进入休眠状态(TIMED_WAITING
),那么前提就是该线程具有正在运行的权利,当然,一个能运行sleep
方法的线程肯定是有运行权并处于RUNNABLE
状态的,因此使用sleep
一般不需要考虑前提条件。
使用场景不同
一个对象(比如 foo
)的 wait
方法一般是在线程中来调用的,因此 foo.wait()
会影响对象 foo
的状态(比如影响 foo
的等待监视器集合),但是线程调用 sleep
影响的是自己的状态(比如从 RUNNABLE
进入 TIMED_WAITING
)。因此,wait
方法(结合相应的 notify
方法)可以用来在不同线程之间进行协作。其中最经典的例子可能就是 Reader-Writer
问题了:
首先定义资源类
class Resources { private String content; public String read () { final String result = this.content; content = null; // 读完内容就设置为空 return result; } public void write (final String content) { this.content = content; } public boolean isEmpty () { return content == null; } }
其中,
content
字段存放资源的内容,通过write
方法写入内容,每次进行read
操作后都将content
置为null
表示资源为空。定义读者类
class Reader extends Thread { private Resources resources; public Reader (final Resources resources) { this.resources = resources; } @Override public void run () { while (true) { try { synchronized (resources) { if (resources.isEmpty ()) { resources.wait (); } System.out.println ("Reader reads " + resources.read ()); resources.notify (); } Thread.sleep (500); } catch (InterruptedException e) { e.printStackTrace (); } } } }
run
方法为一个死循环,不断的读取resources
中的内容;- 为了保证在对资源读取的过程中其它线程(如写线程)不会对资源进行并发操作,需要对资源进行获取锁,使用
synchronized
代码块; - 对资源读取之前首先判断资源是否为空,如果为空则进入该资源的等待监视器集合(
resources.wait()
),否则取出内容打印,并且通知在该对象监视器集合中的其它线程该对象可能改变为其想要的状态; - 最后为了便于观察和避免消耗太多系统资源让线程每次读完休眠半秒。
定义写者类
class Writer extends Thread { private Resources resources; public Writer (final Resources resources) { this.resources = resources; } @Override public void run () { while (true) { try { synchronized (resources) { if (!resources.isEmpty ()) { resources.wait (); } final String content = String.valueOf (System.currentTimeMillis ()); resources.write (content); System.out.println ("Writer writes " + content); resources.notify (); } Thread.sleep (500); } catch (InterruptedException e) { e.printStackTrace (); } } } }
- 同读者大致相同,为了便于观察在写者写资源时将写进去的资源打出来。
编写测试类
public class Test { public static void main (String[] args) throws InterruptedException { final Resources resources = new Resources (); new Reader (resources).start (); new Writer (resources).start (); }
}
上面例子使用了 wait
和 sleep
方法,但是他们的作用很明显,wait
方法是为了 Reader
和 Writer
之间的协作而 sleep
方法仅仅是对本线程的操作。
总结
虽然 wait
和 notify
可以用于解决并发问题,但是自从 JDK 5 以后,JDK 中加入了 java.util.concurrent
包,其中包含了很多控制并发的工具,因此在条件允许的情况下应该优先使用这些并发工具而不是使用 wait
和 notify
。(参见 《Effective Java》)。但是对于接管遗留代码可能需要处理 wait
和 notify
(等待唤醒机制)的使用,因此掌握等待唤醒机制可能也是必要的。