线程通信引发的 java.lang.IllegalMonitorStateException 血案

今天在练习多线程的时候出现了 java.lang.IllegalMonitorStateException 异常,过了很久才意识到问题,记录一下。

问题抛出

经典例题:生产者/消费者问题

  • 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员就会叫生产者停一下,如果店中有空位放产品再通知生产者继续生产;如果店中没有产品了,店员就会告诉消费者等一下,如果店中有产品了再通知小给者来取走产品
  • 这里可能会出现两个问题:
    1. 生产者比消费者块时,消费者会漏掉一些数据没有取到
    2. 消费者比生产者块时,消费者会取相同的数据

实现思路

这里的共享数据,简单的可以任务是商品数量。两个线程分别是生产者线程、消费者线程。
涉及到多个线程访问共享数据,存在线程安全问题,可选择的解决方法有三个:

  1. 同步代码块
  2. 同步方法

但是,可以预料到,生产者线程和消费者线程之间会涉及到通信。那么使用 锁 就不合适了。
因为 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();

    }
}

笔者个人博客,刚刚搭建,欢迎串门
生活不止眼前的苟且

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值