Java线程之为何wait()和notify()必须要用同步块中
就在昨天造轮子的时候,遇到了线程等待和唤醒问题,虽然这是一个基础知识wait() 和notify()/notifyAll() 方法必须用在synchronized所修饰的线程安全的块中。
否则就会报错 IllegalMonitorStateException
既然都知道要这么去做,可是它的原理到底是什么呢?为什么必须再synchronized修饰的块中呢?
经过Bing查阅之后,理解了它的原理。
代码来源: https://programming.guide/java/why-wait-must-be-in-synchronized.html
究竟是什么原理引起的这个异常呢?
这其实是一个臭名昭著的问题 Lost wake-up Problem 这个问题并不是Java语言特有的,而是所有多线程的环境下都会发生的。那么什么是所谓的(丢失唤醒问题)呢?
我们来定义一个队列:Queue<String> buffer = new LinkedList<String>();give() 放入if(buffer.isEmpty()){wait();}return buffer.remove();take() 拿出buffer.add(data);notify();
我们定义两个线程T1 T2,使用到了同一个BlockingQueue对象。
如果运行起来之后,T1如果运行到这里while(buffer.isEmpty()){wait();}
如果T1还没有来得及wait()的时候,突然切换T2,调用了give(),那么就执行添加操作和唤醒操作,这时T2调用了wait()方法,但是由于give()还没有执行到wait()那么就会丢弃掉这个notify()。
我们来通过代码描述一下问题
1 | class BlockingQueue { |
那么这个时候就会发生Lost wake-up Problem 丢失唤醒。如果不是在synchronized block中,就会抛出异常 IllegalMonitorStateException 。说到这里,还是没有说全面,以上只是说了为什么在生产者消费者模型当中要使用synchronized修饰wait()和notify()方法,但是如果具体要说wait()这个方法为什么强制规定在synchronized当中,我们应该从synchronized的实现来说起。
首先我们来认识一下,什么是Monitor对象?
(如果不想看,可以直接跳过,到Synchronized的原理)
(如果不想看,可以直接跳过,到Synchronized的原理)
(如果不想看,可以直接跳过,到Synchronized的原理)
Monitor的特性:
**互斥:**一个 Monitor 锁在同一时刻只能被一个线程占用,其他线程无法占用。
信号机制: 占用 Monitor 锁失败的线程会暂时放弃竞争并等待某个谓词成真(条件变量),但该条件成立后,当前线程会通过释放锁通知正在等待这个条件变量的其他线程,让其可以重新竞争锁。
synchronzied 需要关联一个对象(也就是括号内传入的参数),而这个对象就是 monitor object。
monitor 的机制中,monitor object 充当着维护 mutex以及定义 wait/signal API 来管理线程的阻塞和唤醒的角色。
Java语言中的java.lang.Object类,便是满足这个要求的对象,任何一个 Java 对象都可以作为 monitor机制的monitor object。
Java 对象存储在内存中,分别分为三个部分,即对象头、实例数据和对齐填充,而在其对象头中,保存了锁标识,同时,java.lang.Object类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖于一个叫 ObjectMonitor模式的实现,这是 JVM 内部基于 C++ 实现的一套机制 。
什么是对象头?看一下这篇文章有助于理解: https://www.bilibili.com/read/cv2609116/
当一个线程需要获取 Object的锁时,会被放入 EntrySet 中进行等待,如果该线程获取到了锁,成为当前锁的 owner。如果根据程序逻辑,一个已经获得了锁的线程缺少某些外部条件,而无法继续进行下去(例如生产者发现队列已满或者消费者发现队列为空),那么该线程可以通过调用 wait 方法将锁释放,进入 wait_set 中阻塞进行等待,其它线程在这个时候有机会获得锁,去干其它的事情,从而使得之前不成立的外部条件成立,这样先前被阻塞的线程就可以重新进入 EntrySet 去竞争锁。这个外部条件在 monitor 机制中称为条件变量。
我们可以从JavaDoc看到wait()方法有这么一个说明 This method should only be called by a thread that is the owner of this object’s monitor IllegalMonitorStateException if the current thread is not the owner of the object’s monitor.
为什么添加上synchronized之后就可以避免这个问题了呢?
1 | /*xxxxxxxxxxxxxxxxxxxxxxx*/ |
Synchronized的原理
我们可以将synchronized代码块通过javap生成的字节码,看到其中包含 monitorenter 和 monitorexit 指令,这两个指令就是JVM在底层操作monitor的指令。只有我们获取到了monitor对象之后,才能对被操作对象进行wait操作,添加到waitSet当中。
synchronized是通过获取monitor对象来实现的,这个对象里面有owner,entryList,waitSet等属性,只有拿到对应的monitor对象才能释放他,添加到waitSet(等待列表)里面, 处于wait状态的线程,会被加入到waitSet。
所以,我们调用obj.wait();和obj.notify();方法的时候,必须要获取其对象的监视器(monitor)。synchronized block的底层原理是对monitor进行操作,所以必须要在synchronized block下操作wait()和notify(),原因不仅是Lost wake-up Problem,还因为monitor拥有waitSet。