wait 与 join 的核心区别
这两个关键字都是用来协调线程之间执行顺序的工具,但有着本质区别:
- join:等待另一个线程彻底执行完毕后,当前线程才继续往下执行。
- wait:等待另一个线程执行notify/notifyAll后,当前线程就继续往下执行(不需要等待另一个线程完全执行完毕)
场景引入
假设现在有多个“线程”去ATM中“取钱”。(这就模拟了多个线程竞争同一把锁的场景)
排在最前面的“线程”进入ATM之后,在它出来之前,后面的线程就无法进入。
假设当第一个线程进入之后,发现此时的ATM中并没有钱,那么它就会从ATM中出来。
那么问题来了:当第一个线程出来之后,第二个线程会直接进入ATM中吗?
答案是不一定。
这里我们要明确的一点是,当多个线程竞争同一把锁时,当获取到锁的线程释放之后,其他哪个线程拿到这把锁是随机的,这是由于操作系统的调度是随机的。
此时要注意的是:
- 当前释放这个锁的线程是就绪状态
- 其他线程都属于在这个锁上进行阻塞等待,是阻塞状态
所以当前这个线程是有很大概率继续拿到这把锁的。
如果这个线程一直这样进行“反复横跳”,那么这就会导致其他线程一直不可以在CPU中执行,
这就是所谓的“线程饿死”现象。
上述场景就是wait和notify应用的典型场景
我们可以这样进行优化:当拿到锁的线程发现要执行的任务的时机尚未成熟的时候,就可以使用wait进行阻塞等待,在等待的这段时间里,其他的线程就有机会到CPU上去执行。
这个优化就可以节省许多不必要的开销。
以上述场景为例,当第一个线程发现ATM中并没有钱的时候,它就可以进行wait阻塞等待,当ATM中有钱之后,我们再使用notify关键字把它唤醒。
下面我们结合代码来详细讲解这两个关键字
代码解析
public static void main(String[] args) throws InterruptedException {
Object object=new Object();
System.out.println("wait之前");
object.wait();
System.out.println("wait之后");
}
上述代码运行之后会报错,其中的IllegalMonitorStateException意为“非法的锁状态”。
这是为什么呢?
这是因为object.wait()这行代码在执行的第一步就是先释放object对象所对应的锁,那么能够释放锁的前提是object对象应该处于加锁状态,这样才可以释放。
下面我们给它加一个锁就可以让程序正常运行,示例如下:
Object object=new Object();
System.out.println("wait之前");
synchronized (object){
object.wait();
}
System.out.println("wait之后");
下面我们再来详细解析一下这些代码:
synchronized (object){
object.wait();
}
- 当运行到第一个大括号之后一直到object.wait();这行代码之前一直是加锁状态;
- 当运行到这行代码时(wait在等待的过程中),是解锁状态(尽管这个时候并没有运行到右大括号);
- 当运行完这行代码之后又是加锁状态;
- 运行到右大括号就是解锁状态。
这里还要注意的一个细节是synchronized的锁对象和wait的锁对象必须是同一个~~~
wait和notify结合使用
public static void main(String[] args) {
Object locker1=new Object();
Object locker2=new Object();
Thread t1=new Thread(()->{
try {
System.out.println("wait之前");
synchronized (locker1){
locker1.wait();
}
System.out.println("wait之后");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("请输入任意内容唤醒t1");
scanner.next();
synchronized (locker1){
locker1.notify();
}
});
t1.start();
t2.start();
}
这里的locker1.notify();同样需要先拿到锁,再进行操作(这是Java中给出的限制)
wait是必须要搭配锁来使用的,因为wait需要先释放锁;
而notify操作在原则上说并不涉及到加锁解锁操作。
我们要知道的是,线程和锁本身就是操作系统所支持的特性。
在操作系统原生的API中,wait必须搭配锁来使用而notify则不需要。
我们这里给notify与锁搭配使用,是为了遵循Java给出的限制。
synchronized (locker1){
locker1.wait();
}
synchronized (locker1){
locker1.notify();
}
这里要注意的是,这四处的锁对象必须保持一致。
如果这些锁对象是不同的,那么这两个线程之间就无法建立联系。
上面两个线程的先后顺序是随机的,但是我们要保证的是wait一定要在notify之前执行。
如果先执行notify再执行wait,那么wait将无法被唤醒。
多个线程在同一个对象上进行wait,此时再用notify唤醒
public static void main(String[] args) {
Object locker1=new Object();
Thread t1=new Thread(()->{
System.out.println("wait之前");
synchronized (locker1){
try {
locker1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1wait之后");
});
Thread t2=new Thread(()->{
System.out.println("wait之前");
synchronized (locker1){
try {
locker1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2wait之后");
});
Thread t3=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("请输入任意内容唤醒线程");
scanner.next();
synchronized (locker1){
locker1.notify();
}
});
t1.start();
t2.start();
t3.start();
}
这次唤醒的是t1,经过多次运行后我们发现,当只进行一次唤醒操作的时候,所唤醒的线程是随机的。
要想唤醒全部线程,要么就多执行几次locker1.notify()操作,要么就是用notifyAll()方法。
运行结果分析:
- 当多个线程在同一个对象上等待时,一次 notify 操作只会随机唤醒其中一个线程
- 要唤醒全部线程,可以多次调用notify
()
,或直接使用notifyAll()
方法
虽然使用 notifyAll () 方法可以一次唤醒所有线程,但被唤醒的线程需要重新竞争锁 —— 只有一个线程能成功获取锁并继续执行。
其他线程会因竞争锁失败再次进入阻塞状态,直至持有锁的线程释放锁后重新参与竞争。
总结
wait 与 notify 是 Java 中实现线程间协作的重要机制,它们与锁紧密配合,能够有效解决线程竞争导致的问题(如线程饿死)。
正确理解和使用这两个方法,对于编写高效、可靠的多线程程序至关重要。使用时需特别注意锁对象的一致性和 wait/notify 的调用顺序,以避免程序出现难以调试的并发问题。