java多线程在学习的时候感觉就那么几个类,几个方法,搞了一个生产者消费者例子就去参加校园招聘了。但真正用到后发现学的时候细节没有照顾到,有些问题也没有想过,所以书到用时方恨少。
既然生产者消费者这个例子是课堂上以及部分公司的招聘题目,那么就还是用它举个例子吧,例子不在于多么高大上,能帮助真正吸收就可以。
举个例子,如果你的Java程序中有两个线程——即生产者和消费者,那么生产者可以通知消费者,让消费者开始消耗数据,因为队列缓 冲区中有内容待消费(不为空)。相应的,消费者可以通知生产者可以开始生成更多的数据,因为当它消耗掉某些数据后缓冲区不再为满。
所以在对一个方法,一个对象,或者一个消耗数据添加锁的时候,就要顾虑到是不是锁的同一个对象。如果.wait() 和 .notify()两个方法对应的对象或者消耗数据不是一个的话,就不抛出异常 java.lang.IllegalMonitorStateException
所以: java.lang.IllegalMonitorStateException 异常:
这个异常会在三种情况下抛出:
1>当前线程不含有当前对象的锁资源的时候,调用obj.wait()方法;
2>当前线程不含有当前对象的锁资源的时候,调用obj.notify()方法。
3>当前线程不含有当前对象的锁资源的时候,调用obj.notifyAll()方法。
那我们在避免出现上述异常的情况下,就需要对wait,notify方法的使用规则详细了解。
如何使用Wait
尽管关于wait和notify的概念很基础,它们也都是Object类的函数,但用它们来写代码却并不简单。
例如在错误的地方使用 synchronized 关键词,表现是不知道synchronized同步快怎么使用,同步那个对象呢。
没有用正确的对象调用wait,不知道对什么对象调用wait方法
或者没有遵循规范的代码方法。 关于wait方法是有一个规范的写法的
第一个问题就是,我们怎么在代码里使用wait()呢? 正确的方法是对在多线程间共享的那个Object来调用wait。在生产者消费者问题中,这 个共享的Object就是那个缓冲区队列。
第二个问题是,既然我们应该在synchronized的函数或是对象里调用wait,那哪个对象应该被synchronized呢?答案是,那个 你希望上锁的对象就应该被synchronized,即那个在多个线程间被共享的对象。在生产者消费者问题中,应该被synchronized的就是那个 缓冲区队列。
现在你知道wait应该永远在被synchronized的同步块下和那个被多线程共享的对象上调用,下一个一定要记住的问题就是,你应该永远在 while循环,而不是if语句中调用wait。
因为线程是在某些条件下等待的——在我们的例子里,即“如果缓冲区队列是满的话,那么生产者线程应该等 待”,你可能直觉就会写一个if语句。但if语句存在一些微妙的小问题,导致即使条件没被满足,你的线程你也有可能被错误地唤醒。所以如果你不在线程被唤 醒后再次使用while循环检查唤醒条件是否被满足,你的程序就有可能会出错——例如在缓冲区为满的时候生产者继续生成数据,或者缓冲区为空的时候消费者 开始消耗数据。所以记住,永远在while循环而不是if块中使用wait!
基于以上认知,下面这个是使用wait和notify函数的规范代码模板:
// The standard idiom for calling the wait method in Java
synchronized (sharedObject) {
while (condition) {
sharedObject.wait();
// (Releases lock, and reacquires on wakeup)
}
// do action based upon condition e.g. take or put into queue
}
在while循环里使用wait的目的,是在线程被唤醒的前后都持续检查条件是否被满足。生产者线程如果条件并未改变,那么这个线程并不能被唤醒,可以防止对缓冲区队列误操作。
这是用wait/notifyAll方法对一个列表操作的生产者和消费者模型。
这个方法是对列表进行设置的方法。用一个生产者线程调用这个方法。
public void setValue() {
try {
synchronized (lock) {
while (!ValueObject.value.equals("")) {
// 数据集不为空,那么生产者要休息
System.out.println("生产者 " + Thread.currentThread().getName() + " WAITING ***");
lock.wait();
}
System.out.println("生产者 " + Thread.currentThread().getName() + "RUNNABLE ");
String value = System.currentTimeMillis() + "_" + System.nanoTime();
ValueObject.value = value;
lock.notifyAll();
}
} catch (Exception e) {
// TODO: handle exception
}
}
//这是一个获取列表数据的方法,由消费者线程调用。
public void getValue() {
try {
synchronized (lock) {
while (ValueObject.value.equals("")) {
System.out.println("消费者" + Thread.currentThread().getName() + " WAITING**");
lock.wait();
}
System.out.println("消费者" + Thread.currentThread().getName() + " Runnable");
ValueObject.value = "";
lock.notifyAll();
}
} catch (Exception e) {
// TODO: handle exception
}
}
本文重点:
1. 你可以使用wait和notify函数来实现线程间通信。你可以用它们来实现多线程(>3)之间的通信。
2. 永远在synchronized的函数或对象里使用wait、notify和notifyAll,不然Java虚拟机会生成 IllegalMonitorStateException。
3. 永远在while循环里而不是if语句下使用wait。这样,会在线程暂停恢复后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知。
4. 永远在多线程间共享的对象(在生产者消费者模型里即缓冲区队列)上使用wait。
5. 基于前文提及的理由,更倾向用 notifyAll(),而不是 notify()。用notify()方法只是讲一个因为wait操作处于等待的线程唤醒。可能生产者只是唤醒生产者。而没有唤醒消费者。所以为了防止多线程下假死情况都是用notifyAll()方法
本文是根据个人实际开发工作中遇到的问题,结合网络搜索的资料整合而成,部分见解谨代表个人看法,如有问题,欢迎讨论。
参考文档: