一、wait()和notify()含义
二、标准代码示例
创建两个线程Thread0和Thread1。
代码实现:
运行流程详解
三、什么时候释放锁—wait()、notify()
四、用生活故事讲懂线程的等待唤醒
1.老王和老李(专家程序员):
2.王哥和李哥(普通程序员):
3.小王和小李(新手程序员):
五、问题理解
1、执行wait()的线程,如果重新被唤醒,是从wait()代码之后继续执行的,而不是重新从该方法的头部重新执行。
2、那如果唤醒后的两个线程继续执行while循环,在某个时刻同时判断为数组没满,那不也会抛出越界异常吗?
3、Java多线程为什么使用while循环来调用wait方法
六、阿里巴巴面试题: 为什么wait()和notify()需要搭配synchonized关键字使用
简介
本文讲解Java中wait()、notify(),通过一个标准的使用实例,来讨论下这两个方法的作用和使用时注意点,这两个方法被提取到顶级父类Object对象中,地位等同于toString()方法。
一、wait()和notify()含义
- wait()方法是让当前线程等待的,即让线程释放了对共享对象的锁,不再继续向下执行。
- wait(long timeout)方法可以指定一个超时时间,过了这个时间如果没有被notify()唤醒,则函数还是会返回。如果传递一个负数timeout会抛出IllegalArgumentException异常。
- notify()方法会让调用了wait()系列方法的一个线程释放锁,并通知其它正在等待(调用了wait()方法)的线程得到锁。
- notifyAll()方法会唤醒所有在共享变量上由于调用wait系列方法而被挂起的线程。
注意:
- 调用wait()、notify()方法时,当前线程必须要成功获得锁(必须写在同步代码块锁中),否则将抛出异常。
- 只对当前单个共享变量生效,多个共享变量需要多次调用wait()方法。
- 如果线程A调用wait()方法后处于堵塞状态时,其他线程中断(在其他线程调用A.interrupt()方法)A线程,则会抛出InterruptExcption异常而返回并终止。
二、标准代码示例
创建两个线程Thread0和Thread1。
让Thread0执行wait()方法。
此时Thread1得到锁,再让Thread1执行notify()方法释放锁。
此时Thread0得到锁,Thread0会自动从wait()方法之后的代码,继续执行。
通过上述流程,我们就可以清楚的看到,wait()和notify()各自是怎么工作的了,也可以知道两者是怎么配合的了。
public class ThreadWaitAndNotify { // 创建一个将被两个线程同时访问的共享对象 public static Object object = new Object(); // Thread0线程,执行wait()方法 static class Thread0 extends Thread { @Override public void run() { synchronized (object) { System.out.println(Thread.currentThread().getName() + "初次获得对象锁,执行中,调用共享对象的wait()方法..."); try { // 共享对象wait方法,会让线程释放锁。 object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "再次获得对象锁,执行结束"); } } } // Thread1线程,执行notify()方法 static class Thread1 extends Thread { @Override public void run() { synchronized (object) { // 线程共享对象,通过notify()方法,释放锁并通知其他线程可以得到锁 object.notify(); System.out.println(Thread.currentThread().getName() + "获得对象锁,执行中,调用了共享对象的notify()方法"); } } } // 主线程 public static void main(String[] args) { Thread0 thread0 = new Thread0(); Thread1 thread1 = new Thread1(); thread0.start(); try { // 保证线程Thread0中的wait()方法优先执行,再执线程Thread1的notify()方法 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } thread1.start(); } 运行结果 Thread-0初次获得对象锁,执行中,调用共享对象的wait()方法... Thread-1获得对象锁,执行中,调用了共享对象的notify()方法 Thread-0再次获得对象锁,执行结束
运行流程详解
从执行的结果中,要明白线程的执行顺序:
- Thread0调用了wait()方法后,会释放掉对象锁并暂停执行后续代码,即从wait()方法之后到run()方法结束的代码,都将立即暂停执行,这就是wait()方法在线程中的作用。
- CPU会将对象锁分配给一直等候的Thread1线程,Thread1执行了notify()方法后,会通知其他正在等待线程(Thread0)得到锁,但会继续执行完自己锁内的代码之后,才会交出锁的控制权。
- 因为本例只有两个线程,所以系统会在Thread1交出对象锁控制权后(Synchronized代码块中代码全部执行完后),把锁的控制权给Thread0(若还有其他线程,谁得到锁是随机的,完全看CPU心情),Thread0会接着wait()之后的代码,继续执行到Synchronized代码块结束,将对象锁的控制权交还给CPU。
三、什么时候释放锁—wait()、notify()
由于等待一个锁定线程只有在获得这把锁之后,才能恢复运行,所以让持有锁的线程在不需要锁的时候及时释放锁是很重要的。在以下情况下,持有锁的线程会释放锁:
执行完同步代码块。
在执行同步代码块的过程中,遇到异常而导致线程终止。
在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程会释放锁,进行对象的等待池。
除了以上情况外,只要持有锁的此案吃还没有执行完同步代码块,就不会释放锁。因此在以下情况下,线程不会释放锁:
在执行同步代码块的过程中,执行了Thread.sleep()方法,当前线程放弃CPU,开始睡眠,在睡眠中不会释放锁。
在执行同步代码块的过程中,执行了Thread.yield()方法,当前线程放弃CPU,但不会释放锁。
在执行同步代码块的过程中,其他线程执行了当前对象的suspend()方法,当前线程被暂停,但不会释放锁。但Thread类的suspend()方法已经被废弃。
避免死锁的一个通用的经验法则是:当几个线程都要访问共享资源A、B和C时,保证使每个线程都按照同样的顺序去访问他们,比如都先访问A,再访问B和C。
java.lang.Object类中提供了两个用于线程通信的方法:wait()和notify()。需要注意到是,wait()方法必须放在一个循环中,因为在多线程环境中,共享对象的状态随时可能改变。当一个在对象等待池中的线程被唤醒后,并不一定立即恢复运行,等到这个线程获得了锁及CPU才能继续运行,又可能此时对象的状态已经发生了变化。
# 调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) {...} 代码段内。
# 调用obj.wait()后,线程A就释放了obj的锁,否则线程B无法获得obj锁,也就无法在synchronized(obj) {...} 代码段内唤醒A。
# 当obj.wait()方法返回后,线程A需要再次获得obj锁,才能继续执行。
# 如果A1,A2,A3都在obj.wait(),则B调用obj.notify()只能唤醒A1,A2,A3中的一个(具体哪一个由JVM决定)。
# obj.notifyAll()则能全部唤醒A1,A2,A3,但是要继续执行obj.wait()的下一条语句,必须获得obj锁,因此,A1,A2,A3只有一个有机会获得锁继续执行,例如A1,其余的需要等待A1释放obj锁之后才能继续执行。# 当B调用obj.notify/notifyAll的时候,B正持有obj锁,因此,A1,A2,A3虽被唤醒,但是仍无法获得obj锁。直到B退出synchronized块,释放obj锁后,A1,A2,A3中的一个才有机会获得锁继续执行。
wait()/sleep()的区别
前面讲了wait/notify机制,Thread还有一个sleep()静态方法,它也能使线程暂停一段时间。sleep与wait的不同点是:sleep并不释放锁,并且sleep的暂停和wait暂停是不一样的。obj.wait会使线程进入obj对象的等待集合中并等待唤醒。
但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException。
如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep/join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。
需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到wait()/sleep()/join()后,就会立刻抛出InterruptedException。
四、用生活故事讲懂线程的等待唤醒
Java线程的等待唤醒机制,是通过wait()方法和notify()方法实现的,为了更好的理解,我再来举一个通俗易懂接地气的例子吧,帮不懂代码的人也能明白这两个方法的作用。
例:捡肥皂的故事
假设有两个程序员去洗澡,只带了一块肥皂,两个人怎么使用一块肥皂洗澡呢?会发生3个场景:
1.老王和老李(专家程序员):
老王和老李随机一人拿到肥皂,比如老王先拿到肥皂,然后使用肥皂,然后把肥皂让出去,自己等会再用。老李拿到了肥皂,然后使用了一会,再通知老王说:“自己不用了”,老王听到话以后,捡起肥皂从上次用的地方接着用。二者洗澡,你来我往共享一块肥皂,非常和谐。
程序语言描述:
老王随机先得到锁,然后用了一会后,调用了wait()方法,把锁交了出去,自己等待。老李拿到锁,使用后,再通过notify()通知老王,然后等老李用完以后,老王再次拿到锁,继续执行…这种方式是线程安全的,而且还能合理的分配资源的使用,这就是等待唤醒的好处。
2.王哥和李哥(普通程序员):
王哥和李哥随机一人拿到肥皂,比如王哥先拿到,然后王哥就一直霸占着,直到自己洗完了,才把肥皂给李哥。期间李哥洗澡只能干搓,根本没机会接触肥皂。我想李哥肯定觉得王哥很自私,不懂得礼让,李哥的体验不是很好。
程序语言描述:王哥和李哥就是两个线程,王哥在拿到锁以后,就一直使用,直到同步代码块中的内容完全执行完成。再把锁交给李哥使用。这种方式每次都是一个线程执行完,另一个才会执行,是线程安全的。
3.小王和小李(新手程序员):
小王和小李一开始洗澡就争抢肥皂,当肥皂在小王手上时,小王还在使用中,小李就扑上来了,于是出现了两人一起摩擦一块肥皂的场景!这种画面既不优雅,也不安全。
程序语言描述:
如果两个线程,访问同一个资源的时候,不对其进行加锁控制,就会出现混乱的场景,这就是线程不安全。两个线程可能会同时操作同一共享变量,从而使这个共享变量失控,最终结果紊乱。
五、问题理解
1、执行wait()的线程,如果重新被唤醒,是从wait()代码之后继续执行的,而不是重新从该方法的头部重新执行。
假如有两个线程,向一个数组中插入数据,插入之前先判断数组大小,如果大于等于数组长度,那么wait(),否则会向数组中插入一条数据。还有其他线程是取数据的。
插入数据方法伪代码如下
synchronized void push(int number) {
if (数组满了) {
this.wait();
}
array[++index] = xxx;
}
插入线程1插入直到数组满了,执行wait()方法,然后释放锁,插入线程2判断也满了,然后也执行wait()方法,然后释放锁,这时候假如唤醒的是线程1,那么他就会执行array[++index]==xxx;会数组越界
如果改为
while (数组满了) {
this.wait();
}
就会重新判断一次,如果满了重新wait(),就不会发生异常了
public class ThreadTest implements Runnable{ private static String[] array = new String[1]; private static int index = 0; synchronized void push(String number) { try { System.out.println(Thread.currentThread().getName() + "执行一次"); while (array[array.length-1] != null) { if(Thread.currentThread().getName().equals("t2")){ this.notify(); System.out.println(Thread.currentThread().getName() + "执行"); } this.wait(); System.out.println(Thread.currentThread().getName() + "继续执行"); } array[++index] = number; System.out.println(array[0]); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"执行完成"); } @Override public void run() { push("a"); } public static void main(String[] args) { array[0] = "s"; ThreadTest threadTest = new ThreadTest(); Thread t1 = new Thread(threadTest,"t1"); Thread t2 = new Thread(threadTest,"t2"); t1.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); } } while结果 t1执行一次 t2执行一次 t2执行 t1继续执行 if结果 t1执行一次 t2执行一次 t2执行 t1继续执行 Exception in thread "t1" java.lang.ArrayIndexOutOfBoundsException: 1 at com.crane.ThreadTest.push(ThreadTest.java:27) at com.crane.ThreadTest.run(ThreadTest.java:37) at java.lang.Thread.run(Thread.java:748)
2、那如果唤醒后的两个线程继续执行while循环,在某个时刻同时判断为数组没满,那不也会抛出越界异常吗?
唤醒只是让线程可以去竞争锁了,同步的代码块一个时刻只会有一个线程执行
3、Java多线程为什么使用while循环来调用wait方法
这里的while相当于多次if,这么就好理解一点,防止线程醒来不进行判断,假如你用if,就只判断一次,线程醒来会继续向前走,如果用了while线程醒来会在判断一次条件,符合在走,不符合接着睡!
六、阿里巴巴面试题: 为什么wait()和notify()需要搭配synchonized关键字使用
阿里巴巴面试题: 为什么wait()和notify()需要搭配synchonized关键字使用_萧萧的专栏-CSDN博客
参考:Java多线程wait()和notify()系列方法使用教程(内涵故事)_五道口-CSDN博客
https://www.cnblogs.com/chenjfblog/p/7868875.html