目录
前言
在前面我们已经介绍过了join()方法可以用来控制线程的执行顺序了,但是join()却只能让整个线程执行结束再去执行另外的线程。控制线程执行顺序的效果是有限的,因此又有了wait()和notify(),目的其实都是为了控制程序的执行顺序的。本篇博客就主要来介绍wait()和notify()的使用。
一、举个例子
有一天,A B C三个人去银行的ATM取钱,此时ABC可以理解为三个线程,A进去之后,就把门锁了,接着发现ATM里没钱了,就出来了,就相当于A释放了锁,接着A B C三人又开始竞争锁了,这时候谁抢到锁,那就是随机的了。这时候就可能就会出现极端情况:A运气好,一直都抢到锁,一直在ATM中进进出出,B C线程就一直没被执行到,此时B C就称为线程饿死,这样效率就变低了。此时我们使用wait()就可以解决这个问题了,具体来说就是:A进去之后,发现ATM里没钱了,就wait(),也就是阻塞等待了,这时候就释放锁了,在阻塞等待过程中就不会和B C竞争锁了。这时候BC就有机会拿到锁了,就可以执行了。A阻塞等待一段时间之后,这时候可能就有工作人员去把钱存到ATM里面了或者有人往ATM里存钱了,这时候别的人就告诉A说可以进去取钱了,(也就是当其他线程发现A的条件满足时,就调用notify去唤醒A,这就是notify的用法)。这就是wait和notify的大概使用场景。
二、wait()和notify()
2.1wait()的介绍
wait()方法其实主要做三件事:
①把当前线程放进等待队列中
②释放锁
③被其他线程唤醒时,尝试重新获取锁
接下来看一段问题代码:
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
System.out.println("wait()之前");
object.wait();
System.out.println("wait()之后");
}
运行结果:
代码分析:我们可以看到,这个代码只执行到了打印wait()之前,接着就抛出了一个异常,这个异常是非法的锁状态异常,这是因为,因为这时候我们压根没有对wait()这个方法加锁,就尝试着要去解锁(wait()方法会释放锁),这完全是不符合逻辑的,因此就抛出了异常。所以wait()是需要搭配synchronizaed使用的。
2.2wait()和notify()的基本使用
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();//创建一个锁对象
Thread t1 = new Thread(()->{
System.out.println("wait()之前");
synchronized (locker){//先进行加锁操作
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("wait()之后");
});
t1.start();
Thread.sleep(1000);//等待1s确保t1已经执行到wait
Thread t2 = new Thread(()->{
System.out.println("notify()之前");
synchronized (locker){
locker.notify();//唤醒线程t1
}
System.out.println("notify()之后");
});
t2.start();
}
运行结果:我们在这里先创建了一个锁对象,这里执行过程是这样的wait()执行之后,t1进入阻塞等待,并释放了锁,接着就开始执行t2,t2开始执行之后就获取了锁,接着调用了notify()唤醒了t1线程,并且释放了锁,然后t1被唤醒了之后就继续往下执行了。
注意事项:在上面代码中,在wait时候线程会进入WAITING状态,wait、synchronized对象必须是同一个才能正确的加锁,wait和notify的对象也必须是同一个,才可以正确唤醒。而且必须保证wait是在notify之前,如果notify执行在wait之前,程序不会报错,也不会抛异常,相当于是一次无效操作。
2.3wait()的带参数版本
wait() 无参数版本
wait(long) 指定最大等待时间,单位毫秒
wait(long,int) 精度更高,前面是毫秒,后面是纳秒
我们上面的例子wait()是一个不带参数的版本,也就意味着这里的wait()是一个死等的状态,但在实际使用的时候,我们是很少会让线程死等的。我们一般使用wait()的有参数版本,指定最大等待时间。
2.4 notify()和notifyAll()
notify()和notifyAll()这两个方法通过方法名就很容易可以看出来区别,前者是唤醒一个线程,后者是唤醒全部的线程。
notifyAll的一个例子:
开头我们讲了三个人A B C三个人同时去取钱,当三个人都进去ATM里面看完之后,发现ATM(三个线程wait了)里面没钱,就都会进入阻塞等待状态,而当工作人员将钱放入ATM里面之后,这时候工作人员去通知这三个人来取钱了(此时就相当于调用notifyAll方法了,这里唤醒之后三个线程是会竞争锁的而不是同时执行的)。
注意事项:
一般情况下,我们都是使用notify,因为notifyAll唤醒全部,要慎重使用。
三、wait和sleep的对比
wait和sleep其实是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间。这两个方法设计的初心是不同的,一个是单纯为了让线程进行阻塞等待而休眠一段时间,而一个是为了解决线程的执行顺序的问题。最明显的区别就是wait需要搭配锁使用,而sleep不需要。wait 是 Object 的方法 sleep 是 Thread 的静态方法。他们唯一的相同点就是都可以让线程进入阻塞等待状态。