今天在练习多线程的时候出现了 java.lang.IllegalMonitorStateException 异常,过了很久才意识到问题,记录一下。
问题抛出
经典例题:生产者/消费者问题
- 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员就会叫生产者停一下,如果店中有空位放产品再通知生产者继续生产;如果店中没有产品了,店员就会告诉消费者等一下,如果店中有产品了再通知小给者来取走产品
- 这里可能会出现两个问题:
- 生产者比消费者块时,消费者会漏掉一些数据没有取到
- 消费者比生产者块时,消费者会取相同的数据
实现思路
这里的共享数据,简单的可以任务是商品数量。两个线程分别是生产者线程、消费者线程。
涉及到多个线程访问共享数据,存在线程安全问题,可选择的解决方法有三个:
- 同步代码块
- 同步方法
- 锁
但是,可以预料到,生产者线程和消费者线程之间会涉及到通信。那么使用 锁 就不合适了。
因为 wait() notify() notifyAll() 方法必须使用在 同步代码块 或者 同步方法中。
笔者选择使用同步代码块,你要是用同步方法也完全ok
错误代码一览
先请大家看看错误代码,此处我会详尽注释,力求清晰
package com.atguigu.java2;
/**
* 生产者消费者问题
*/
//这个就是那个店员,重点是它维护了一个商品数量
class Clerk{
private int num;
public Clerk(int num) {
this.num = num;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
}
/**
* 这里使用继承Thread类方式来创建线程
* 实际上使用实现Runnable接口的方式更好
* 原因:
* 1.实现的方式不存在单一继承的困扰
* 2.实现的方式天生的有利于操作共享资源
**/
class Producer extends Thread {
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true) {
synchronized (clerk) {
if (clerk.getNum() < 20) {
clerk.setNum(clerk.getNum()+1);
System.out.println(getName() + ":生产了1件产品,当前共有" + clerk.getNum());
}else {
try {
System.out.println("当前商品数量: " + clerk.getNum() + ",停止生产,等待消费");
//大于20等待
notify();
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
//这里是消费者,跟生产者使用的方式相同
class Consumer extends Thread {
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true) {
synchronized (clerk) {
if (clerk.getNum() > 0) {
clerk.setNum(clerk.getNum()-1);
try {
//让消费者稍微睡一会,这样两个线程才有差
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":消费了1件产品,当前共有" + clerk.getNum());
}else {
try {
System.out.println("当前商品数量:" + clerk.getNum() + ",停止消费,等待生产");
notify();
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
//简单的测试类
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk(0);
Producer p1 = new Producer(clerk);
p1.setName("生产者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消费者1");
p1.start();
c1.start();
}
}
代码解释
此处解释是为了更加清晰,如果能清晰看懂,那么,请跳过~~~
此处笔者使用继承Thread的方式创建线程,众所周知。这种方式往往需要实例化多个对象,才能够创建多个线程,那么笔者此处操作的是不是共享对象呢?
仔细观察生产者对象和消费者对象。两者中都有一个属性,同时分别提供了一个有参构造器,参数是clerk(店员)。
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
而在main()方法中,虽然生产者、消费者各实例化了一个对象,但是他们所使用的clerk是同一个。
因此,笔者此处操作的是共享对象,而这个共享对象,是clerk。
Clerk clerk = new Clerk(0);
Producer p1 = new Producer(clerk);
p1.setName("生产者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消费者1");
错在哪?Why?
可能很多读者已经看出这段代码的问题,可以跳过~~,如果没有,可以本地跑一下
下面分析一下错误原因。
首先抛出的异常是 java.lang.IllegalMonitorStateException。
很容易知道这是因为线程通信抛出的异常,那么我们检查一下。
@Override
public void run() {
while (true) {
synchronized (clerk) {
if (clerk.getNum() < 20) {
clerk.setNum(clerk.getNum()+1);
System.out.println(getName() + ":生产了1件产品,当前共有" + clerk.getNum());
}else {
try {
System.out.println("当前商品数量: " + clerk.getNum() + ",停止生产,等待消费");
//大于20等待
notify();
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
我们知道 wait() notify() notifyAll()方法必须调用在同步代码块或者同步方法中,此处我们调用在同步代码块中,所以这里没有问题。
再分析,此时我们是使用 clerk 这个唯一对象锁定了两个代码块(也就是锁定两个线程)。
再看,此时我们调用notify() wait()方法时,省略了调用对象,那么是哪个对象调用的这两个方法?是clerk这个唯一对象吗?还是this ? 这里的this 是 clerk这个唯一对象吗?
谜底揭晓
很多读者可能已经认识到了。
此处我们加锁的对象是 clerk 。没错,这是一个唯一对象
此处调用notify() wait() 方法,省略的调用者是 this 。没错,确实是this
那么,重点来了,此处 this 就是 clerk吗?
并不是!!!
Producer p1 = new Producer(clerk);
p1.setName("生产者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消费者1");
p1.start();
c1.start();
此处,生产者对象和消费者对象继承的Thread类。调用start()方法开启线程,继而调用run()方法。即run()方法调用者分别是 p1 ,c1 。也就是说this 分别是 p1 ,c1。
再想,**我们在clerk对象加锁,却在其他未加锁对象上wait() notify() **。 这显然不合理。
因此,抛出java.lang.IllegalMonitorStateException异常。
如何解决
分析出问题的根源,解决就很简单了。
我们已经找到了加锁对象 clerk ,只要在clerk上 wait() notify()即可。
正确代码一览
package com.atguigu.java2;
/**
* 生产者消费者问题
*/
class Clerk{
private int num;
public Clerk(int num) {
this.num = num;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
}
class Producer extends Thread {
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true) {
synchronized (clerk) {
if (clerk.getNum() < 20) {
clerk.setNum(clerk.getNum()+1);
System.out.println(getName() + ":生产了1件产品,当前共有" + clerk.getNum());
}else {
try {
System.out.println("当前商品数量: " + clerk.getNum() + ",停止生产,等待消费");
//大于20等待
clerk.notify();
clerk.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
class Consumer extends Thread {
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true) {
synchronized (clerk) {
if (clerk.getNum() > 0) {
clerk.setNum(clerk.getNum()-1);
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":消费了1件产品,当前共有" + clerk.getNum());
}else {
try {
System.out.println("当前商品数量:" + clerk.getNum() + ",停止消费,等待生产");
clerk.notify();
clerk.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk(0);
Producer p1 = new Producer(clerk);
p1.setName("生产者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消费者1");
p1.start();
c1.start();
}
}
笔者个人博客,刚刚搭建,欢迎串门
生活不止眼前的苟且