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()
我们来通过代码描述一下问题

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对象来实现的,这个对象里面有ownerentryListwaitSet等属性,只有拿到对应的monitor对象才能释放他,添加到waitSet(等待列表)里面, 处于wait状态的线程,会被加入到waitSet
所以,我们调用obj.wait();obj.notify();方法的时候,必须要获取其对象的监视器(monitor)。
synchronized block的底层原理是对monitor进行操作,所以必须要在synchronized block下操作wait()notify(),原因不仅是Lost wake-up Problem,还因为monitor拥有waitSet

标签:

发表评论

电子邮件地址不会被公开。 必填项已用*标注

Captcha Code