就在昨天造轮子的时候,遇到了线程等待和唤醒问题,虽然这是一个基础知识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()
。
我们来通过代码描述一下问题
class BlockingQueue { Queue<String> buffer = new LinkedList<String>(); public void give(String data) { buffer.add(data); notify(); //如果T2突然切到这个方法,那么这个notify()谁都没唤醒 } public String take() throws InterruptedException { while (buffer.isEmpty()){//如果队列为空 //**如果T1执行到这里T2突然夺得权限执行give();方法, //那么还没来得及执行wait,就notify()了 wait();// } return buffer.remove(); } } class Operate{ static BlockingQueue bq = new BlockingQueue(); public void xxx(){ /* 这里操作BlockingQueue对象 */ } } public class Main{ public static void main(String[] args){ Thread t1 = new Thread(new Operate()); Thread t2 = new Thread(new Operate()); t1.start(); t2.start(); } }
那么这个时候就会发生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之后就可以避免这个问题了呢?
/*xxxxxxxxxxxxxxxxxxxxxxx*/ synchronized(this){ wait(); } /*xxxxxxxxxxxxxxxxxxxxxxx*/ synchronized(this){ notify(); }
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
。