这几个方法都是Object的方法,主要配合synchronized同步一起使用。这涉及到Java的锁机制,可以参考我的一篇博文 Java多线程 - 锁机制。
我们接下来看看这几个方法:
(1)wait
public final void wait() throws InterruptedException,IllegalMonitorStateException
线程要调用Object实例对象的wait方法前,必须要先获取该Object实例对象锁,否则报IllegalMonitorStateException异常,所以wait方法必须在获取Object实例对象锁的同步块中调用。在线程调用Object实例对象的wait方法后,线程便进入“等待”状态,直到收到通知或者中断为止。
// ① wait 实质
在线程(如A)调用Object实例(如lockA)的wait方法后,线程(A)会进入实例对象(lockA)的等待池中,
而等待池的线程处于wait状态,不会主动去获取Object实例(lockA)对象锁,等待notify或者notifyAll。
所以线程(A)等待的不是Object实例(lockA)对象锁,等待的是有其他线程调用Object实例(lockA)的notify或者notifyAll通知。
例子:
public static class RunnableA implements Runnable {
private Object lockA;
public RunnableA(Object lockA) {
this.lockA = lockA;
}
@Override
public void run() {
synchronized (lockA) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println("RunnableA before wait lockA");
try {
// 必须在 synchronized (lockA) {} 内调用,线程会在这里阻塞,等待通知
lockA.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 在其他线程没有调用notify或者notifyAll方法之前,不会执行
System.err.println("RunnableA after wait lockA");
}
}
}
(2)notify
public final native void notify() throws IllegalMonitorStateException
和wait方法一样,调用之前一定要先获取Object实例对象锁,否则报IllegalMonitorStateException错误。
在线程调用Object实例对象notify方法后,会通知等待该Object实例对象锁的其他线程(也就是调用过该Object实例的wait方法处于等待的线程),获取到该Object对象锁的线程回继续执行wait方法后面的语句。
需要注意的是,在线程(A)调用Object实例的notify方法后,并不会立刻释放Object实例对象锁,synchronized同步块的代码会执行完了(跳出synchronized同步块)之后,线程(A)才会释放Object实例对象锁,线程(B,任意一个处于wait状态的线程)才能获取到Object实例对象锁,继续往下执行。
如果有多个线程(A,B,C)都处于wait状态,任意一个线程(如A)会收到通知,其他线程(B,C)的仍然处于wait状态。即使Object实例对象锁闲置了,其他线程(B,C)也不会主动改变状态,也不会主动去获取Object实例对象锁。除非有线程(A)调用了Object实例notify方法,wait的线程(B,C)才机会收到通知,继续执行。
// ② notify 实质
// 结合 ① wait 实质
在线程(如B)调用Object实例(lockA)的notify方法后,收到通知的线程(A)会被移到
实例对象(lockA)的锁池,在这个池的线程会竞争实例对象(lockA)锁。竞争到锁的线程也会被移出锁池,
继续执行wait后面的语句。
例子:
public static class RunnableB implements Runnable {
private Object lockA;
public RunnableB(Object lockA) {
this.lockA = lockA;
}
@Override
public void run() {
synchronized (lockA) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println("RunnableB notify lockA");
// 必须在 synchronized (lockA) {} 内调用
lockA.notify();
// 即使调用notify通知其他线程,这段代码还是会先执行,因为还没有释放lockA锁
System.err.println("RunnableB after notify lockA");
}
}
}
在给出main方法:
public static void main(String[] args) {
// 实例对象
Object lockA = new Object();
// RunnableA和RunnableB持有同一个实例对象
Thread threadA = new Thread(new RunnableA(lockA));
Thread threadB = new Thread(new RunnableB(lockA));
// 启动threadA和threadB
threadA.start();
threadB.start();
}
打印结果:
RunnableA before wait lockA // RunnableA开始等待,并释放lockA实例对象锁
RunnableB notify lockA // RunnableB执行
RunnableB after notify lockA // RunnableB继续执行,释放lockA实例对象锁
RunnableA after wait lockA // RunnableA收到通知,获取到lockA实例对象锁,继续执行wait后面的语句
(3)notifyAll
public final native void notifyAll() throws IllegalMonitorStateException
和wait,notify方法一样,调用之前一定要先获取Object实例对象锁,否则报IllegalMonitorStateException错误。
在线程调用Object实例对象notifyAll方法后,会通知等待该Object实例对象锁的所有线程(也就是调用过该Object实例的wait方法处于等待的所有线程)。
根据notify的实质原理,有2个线程(A,B)调用了Object实例对象(lockA)的wait方法,此时,这2个线程(A,B)就被移到了Object实例对象(lockA)的等待池中。有个线程(C)调用了Object实例对象(lockA)的notifyAll方法,此时,处于wait状态的2个线程(A,B)都会被移到Object实例对象(lockA)的锁池中。那么这2个线程(A,B)就会开始竞争Object实例对象(lockA)锁。如果其中一个线程(A)争到了,执行wait后面的语句,跳出synchronized同步代码块,释放锁。此时的另一个线程(B)还在Object实例对象(lockA)的锁池中,因为Object实例对象(lockA)锁已经闲置了,线程(B)便可以获取Object实例对象(lockA)锁,也接着执行wait后面的语句。
所以在线程中调用Object实例对象的notifyAll方法,会让所有处于wait状态的线程都有机会竞争实例对象锁。
(4)wait(long)和wait(long,int)
这两个方法是设置等待超时时间的,后者在超值时间上加上ns,精度也难以达到,因此,该方法很少使用。对于前者,如果在等待线程接到通知或被中断之前,已经超过了指定的毫秒数,则它通过竞争重新获得锁,并从wait(long)返回。另外,需要知道,如果设置了超时时间,当wait()返回时,我们不能确定它是因为接到了通知还是因为超时而返回的,因为wait()方法不会返回任何相关的信息。但一般可以通过设置标志位来判断,在notify之前改变标志位的值,在wait()方法后读取该标志位的值来判断,当然为了保证notify不被遗漏,我们还需要另外一个标志位来循环判断是否调用wait()方法。
(5)sleep和wait的区别
两者都可以让线程阻塞,但是有2个明显的区别:
sleep是Thread类的方法,而wait是Object类的方法,根本不是同一类的方法。
sleep方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep方法的过程中,线程不会释放对象锁。而当调用wait方法的时候,线程会放弃对象锁,进入等待此对象的等待池,只有针对此对象调用notify方法后本线程才进入对象锁池准备。
(6)巩固
下面再给出一个例子(网上搜的),巩固一下我们对wait和notify的使用:
public class MyThreadPrinter implements Runnable {
private String name;
private Object prev;
private Object self;
private MyThreadPrinter(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
@Override
public void run() {
int count = 10;
while (count > 0) {
synchronized (prev) {
synchronized (self) {
System.out.print(name);
count--;
self.notify();
}
try {
prev.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws Exception {
Object a = new Object();
Object b = new Object();
Object c = new Object();
MyThreadPrinter pa = new MyThreadPrinter("A", c, a);
MyThreadPrinter pb = new MyThreadPrinter("B", a, b);
MyThreadPrinter pc = new MyThreadPrinter("C", b, c);
new Thread(pa).start();
Thread.sleep(100); //确保按顺序A、B、C执行
new Thread(pb).start();
Thread.sleep(100);
new Thread(pc).start();
Thread.sleep(100);
}
}
我们来分析一下上面的例子。
(1)pa(启动或唤醒),先获取prev(c)对象锁(同时持有c对象锁是pc线程。第一次获取时,pc没有启动;第一次之后,pc早已经执行完synchronized(self)代码块,自然已经释放了c对象锁),然后获取self(a)对象锁(同时持有a对象锁是pb线程。第一次获取时,pb没有启动;第一次之后,pb调用a的wait方法,处于阻塞状态,释放了a对象锁),再调用self(a)的notify,唤醒pb(第一次启动时,pb并没有启动;第一次之后,pb处于wait状态,便收到通知,开始唤醒),最后调用prev(c)的wait方法,开始阻塞,等待prev(c)的notify或者notifyAll。
(2)pb(启动或唤醒),先获取prev(a)对象锁(同时持有a对象锁是pa线程。此时的pa处于wait状态,而且已经执行完synchronized(self)代码块,不再持有a对象锁),然后再获取self(b)对象锁(同时持有a对象锁是pc线程。第一次获取时,pc没有启动;第一次之后,pc调用b的wait方法,处于阻塞状态,释放b对象锁),再调用self(b)的notify,唤醒pc(第一次启动时,pc并没有启动;第一次之后,pc处于wait状态,便收到通知,开始唤醒),最后调用prev(a)的wait方法,开始阻塞,等待prev(a)的notify或者notifyAll。
(3)pc(启动或唤醒),先获取prev(b)对象锁(同时持有b对象锁是pb线程。此时的pb处于wait状态,而且已经执行完synchronized(self)代码块,不再持有b对象锁),然后再获取self(c)对象锁(同时持有c对象锁是pa线程。pa调用c的wait方法,处于阻塞状态,释放c对象锁),再调用self(c)的notify,唤醒pa(pa处于wait状态,便收到通知,开始唤醒),最后调用prev(b)的wait方法,开始阻塞,等待prev(b)的notify或者notifyAll。因为pa收到唤醒通知,有回到(1)。
所以运行的结果是ABCABCABCABC…
到这里,相信你也会很自如的使用wait和notify了。