今天在練習多線程的時候出現了 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();
}
}
筆者個人博客,剛剛搭建,歡迎串門
生活不止眼前的苟且