線程通信引發的 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();

    }
}

筆者個人博客,剛剛搭建,歡迎串門
生活不止眼前的苟且

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章