由于操作系统对线程的调度是随机执行的,且线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知。但是,有时候在实际开发中,我们希望合理的协调多个线程之间的先后执行顺序。在Java中,wait()方法和notify()方法就是解决该问题的。
1.wait()方法
线程饿死:线程饿死也叫线程饥饿,当多个线程同时竞争一把锁的时候,由于操作系统对线程的调度是随机的,所以当获取锁的线程释放锁之后,接下来哪个线程会拿到锁,这是不确定的。但是由于其他线程都属于在锁上阻塞等待,处于阻塞状态,而当前释放锁的线程处于就绪状态,这个线程还是有很大概率拿到该锁的。这样就会导致其他线程一直吃不到CPU资源,出现线程饿死的现象。
语法形式:
syncronized(锁对象){
锁对象.wait();
}
虽然我们无法干预调度器对于线程的调度,但是我们可以通过调用wait()方法,然后面的逻辑先执行,等后面的逻辑先执行完之后,当前面的逻辑收到通知,再继续执行。
wait()所作的事情
1.让调用wait方法的线程进行等待(将该线程放到等待队列中)
2.释放当前线程所持有的锁
3.满足一定条件被唤醒之后,线程再此重新获取该锁
当一个线程调用对象的wait方法时,该线程会释放该对象的监视器锁,并进入等待队列中等待被唤醒。
注意:由于使用wait()方法会释放锁,所以,再Java中,使用notify()方法之前,我们先加锁,要搭配syncronized使用。否则会报出一个非法锁状态的异常。
如下图
wait()和join()的区别
两个方法都是等,但是join方法必须要等待另一个线程全部执行完之后,代码才能继续走下去,但是wait方法不一定另一个线程全部执行完,只需要下面的逻辑执行到notify方法,代码就可以继续走下去。
syncronized不也是等待吗?
我们要知道,有时尽管写了syncronized,但是它不一定触发等待,因为我们不确定别的线程是否为加锁状态。如果其他线程没有处于加锁状态,那么该线程就直接获取到锁,直接加锁,这个过程中并没有等待。
wait方法的结束条件:
1.其他线程调用notify方法
2.其他线程调用该线程的interrupted方法,会导致wait抛出InterruptedException异常,导致wait被唤醒,同时也导致该线程结束。
3.wait方法也提供了带参数版本,来指定等待的时间。这时只要等待的时间到了,线程就会自动唤醒,不用notify来通知唤醒。
当代码执行到wait方法后,该线程会一直等待下去,那么我们肯定不能让该线程继续等待下去,这时,我们就要用到notify方法去唤醒线程了。
2.notify()方法
语法:
syncronized(锁对象){
锁对象.notify();
}
notify()方法是用来唤醒因为wait方法而阻塞的线程。
注意事项:
notify()方法也要搭配syncronized()使用,这是Java中特殊规定的。
使用notify()之前,务必要确保先wait了,否则,notify()方法就没起到唤醒的作用,但是也不会有副作用(抛异常)
如果有多个线程在同一个锁对象上进行了wait,那么,notify()会随机唤醒多个线程中的一个线程。
代码执行到notify()方法后,当前线程并不会马上释放锁,而是要等到执行完notify()方法的线程将程序执行完之后才会释放锁,也就是退出同步代码块(syncronied修饰的代码块)之后才会释放对象锁。
wait方法和notify方法锁对象要是一个锁对象,才会起作用
被唤醒的线程不会立即获得对象的监视器锁并继续执行,而是从等待队列中移出,进入与其他线程竞争锁的状态。只有当该线程获得了对象的监视器锁后,才能继续执行wait之后的代码。
例子:
java
public class Demo9 {
public static void main(String[] args) {
Object locker=new Object();
Thread t1=new Thread(()->{
synchronized (locker){
System.out.println("t1在wait之前");
try {
System.out.println("t1执行到wait,释放锁");
locker.wait();
System.out.println("t1被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2=new Thread(()->{
synchronized (locker){
System.out.println("t2获取到锁");
try {
locker.wait();
System.out.println("t2被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t3=new Thread(()->{
synchronized (locker){
System.out.println("t3唤醒t1");
locker.notify();
System.out.println("同步代码块执行完,t3释放锁");
}
});
t1.start();
t2.start();
t3.start();
}
}
根据代码运行结果分析代码逻辑:线程t1启动并获取到锁,进行加锁,接着线程t2也启动,也尝试获取锁,由于t1没有释放锁,t2就阻塞了。接着在线程t1中,执行到wait()方法,t1此时就释放锁,然后线程t3就获取到了锁,当在线程t3中执行到notify()方法,去唤醒t1,当t3中的同步代码块里面的逻辑执行完后,就释放锁 ,由于t1被唤醒需要时间,在这段时间内,由于t1没被唤醒,无法获取到锁,线程t2就获取到了锁,线程t2就会执行到wait,wait方法就会导致线程t2释放锁,这回t1就已经被唤醒了,就会获取到锁,线程t1就会继续执行下去。但是线程t2由于没有notify方法去唤醒,所以它就一直处于睡眠状态,不会被唤醒。
2.1notifyAll()
如果我们想要一次唤醒多个线程在同一个锁对象进行wait,我们就可以使用notifyAll()方法。
如修改上面代码,将线程t1和线程t2全部唤醒。
java
public static void main(String[] args) {
Object locker=new Object();
Thread t1=new Thread(()->{
synchronized (locker){
System.out.println("t1在wait之前");
try {
locker.wait();
System.out.println("t1被notifyAll()唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2=new Thread(()->{
synchronized (locker){
System.out.println("t2在wait之前");
try {
locker.wait();
System.out.println("t2被notifyAll()唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t3=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("请输入唤醒所有线程");
scanner.next();
synchronized (locker){
locker.notifyAll();
System.out.println("notifyall方法后");
}
});
t1.start();
t2.start();
t3.start();
}
注意事项:notifyAll()方法在唤醒所有在等待队列中的线程时,这几个线程是存在锁竞争的,只有一个线程能获得锁,其余线程则会继续阻塞,继续尝试获取锁。
3.wait()和sleep()的区别
1.wait方法需要搭配锁来使用,先加锁,之后才能wait,而sleep使用前不需要加锁。
2.如果都是在syncronized内部使用,wait会释放锁,而sleep方法不会释放锁。