本篇博客为《Java并发编程之美》学习笔记
wait()函数
当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:
其他线程调用了该共享对象的notify()或者notifyAll()方法;其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。另外需要注意的是,如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常。
那么一个线程如何才能获取一个共享变量的监视器锁呢?
执行synchronized同步代码块时,使用该共享变量作为参数。
synchronized(共享变量){ // doSomething }调用该共享变量的方法,并且该方法使用了synchronized修饰。
synchronized void add(int a, int b){ // doSomething }虚假唤醒
一个线程可以从挂起状态变为可运行状态(也就是被唤醒),即使该线程没有被其他线程调用notify()、notifyAll()方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒。
虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用wait()方法进行防范。退出循环的条件是满足了唤醒该线程的条件。
// obj为共享变量 synchronized(obj){ while(该线程被唤醒的条件不满足){ obj.wait(); } }下面从一个简单的生产者和消费者例子来加深理解。如下面代码所示,其中queue为共享变量,生产者线程在调用queue的wait()方法前,使用synchronized关键字拿到了该共享变量queue的监视器锁,所以调用wait()方法才不会抛出IllegalMonitorStateException异常。
如果当前队列没有空闲容量则会调用queued的wait()方法挂起当前线程,这里使用循环就是为了避免上面说的虚假唤醒问题。假如当前线程被虚假唤醒了,但是队列还是没有空余容量,那么当前线程还是会调用wait()方法把自己挂起。
synchronized(queue){ while(queue.size() == MAX_SIZE){ // 这个就是唤醒的条件,当满的时候就是不满足 try{ // 挂起当前线程,并释放通过同步块获取的queue上的锁,让消费者线程可以获取该锁,然后获取队列中的元素 queue.wait(); }catch(Exception ex){ ex.printStackTrace(); } } // 空闲则生成元素,并通知消费者线程 queue.add(element); queue.notifyAll(); } synchronized(queue){ while(queue.size() == 0){ try{ // 挂起当前线程,并释放通过同步块获取的queue上的锁,让生产者线程可以获取该锁,将生产元素放入队列 queue.wait(); }catch(Exception ex){ ex.printStackTrace(); } } // 消费元素,并通知唤醒生产者线程 queue.take(); queue.notifyAll(); }在如上代码中假如生产者线程A首先通过synchronized获取到了queue上的锁,那么后续所有企图生产元素的线程和消费线程将会在获取该监视器锁的地方被阻塞挂起。线程A获取锁后发现当前队列已满会调用queue.wait()方法阻塞自己,然后释放获取的queue上的锁。
这里考虑下为何要释放该锁?
如果不释放,由于其他生产者线程和所有消费者线程都已经被阻塞挂起,而线程A也被挂起,这就处于了死锁状态。这里线程A挂起自己后释放共享变量上的锁,就是为了打破死锁必要条件之一的持有并等待原则。线程A释放锁后,其他生产者线程和所有消费者线程中会有一个线程获取queue上的锁进而进入同步块,这就打破了死锁状态。
另外需要注意的是,当前线程调用共享变量的wait()方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。下面来看一个例子:
/** * @Author Hory * @Date 2020/10/6 */ public class testResource { // 创建资源 private static volatile Object resourceA = new Object(); private static volatile Object resourceB = new Object(); public static void main(String[] args) throws InterruptedException{ // 创建线程 Thread threadA = new Thread(new Runnable(){ public void run(){ try{ // 获取共享资源resourceA的监视器锁 synchronized(resourceA){ System.out.println("threadA get resourceA lock"); // 获取共享资源resourceB的监视器锁 synchronized(resourceB){ System.out.println("threadA get resourceB lock"); // 线程A阻塞,并释放获取到的resourceA的锁 System.out.println("threadA release resourceA lock"); resourceA.wait(); } } }catch(InterruptedException e){ e.printStackTrace(); } } }); // 创建线程 Thread threadB = new Thread(new Runnable(){ public void run(){ try{ // 休眠1s Thread.sleep(1000); // 获取共享资源resourceA的监视器锁 synchronized(resourceA){ System.out.println("threadB get resourceA lock"); System.out.println("threadB try get resourceA lock"); // 获取共享资源resourceB的监视器锁 synchronized(resourceB){ System.out.println("threadA get resourceB lock"); // 线程B阻塞,并释放获取到的resourceA的锁 System.out.println("threadB release resourceB lock"); resourceA.wait(); } } }catch(InterruptedException e){ e.printStackTrace(); } } }); // 启动线程 threadA.start(); threadB.start(); // 等待两个线程结束 threadA.join(); threadB.join(); System.out.println("main over"); } }运行结果:
threadA get resourceA lock threadA get resourceB lock threadA release resourceA lock threadB get resourceA lock threadB try get resourceA lock如上代码中,在main函数里面启动了线程A和线程B,为了让线程A先获取到锁,这里让线程B先休眠了1s,线程A先后获取到共享变量resourceA和共享变量resourceB上的锁,然后调用了resourceA的wait()方法阻塞自己,阻塞自己后线程A释放掉获取的resourceA上的锁。
线程B休眠结束后会首先尝试获取resourceA上的锁,如果当时线程A还没有调用wait()方法释放该锁,那么线程B会被阻塞,当线程A释放了resourceA上的锁后,线程B就会获取到resourceA上的锁,然后尝试获取resourceB上的锁。由于线程A调用的是resourceA上的wait()方法,所以线程A挂起自己后并没有释放获取到的resourceB上的锁,所以线程B尝试获取resourceB上的锁时会被阻塞。
这就证明了当线程调用共享对象的wait()方法时,当前线程只会释放当前共享对象的锁,当前线程持有的其他共享对象的监视器锁并不会被释放。