Java~Thread的API是如何改變執行緒的狀態和分析解決生產者|消費者模型中的假死現象與wait條件改變異常

2020-10-06 11:00:06

首先了解Thread的API是如何改變執行緒的狀態的

在這裡插入圖片描述

  • 1)新建立一個新的執行緒物件後,再呼叫它的start()方法,系統會為此執行緒分配CPU資源,使其處於Runnable(可執行)狀態,這是一個準備執行的階段。如果執行緒搶佔到CPU資源,此執行緒就處於Running(執行)狀態。
  • 2)Runnable狀態和Running狀態可相互切換,因為有可能執行緒執行一段時間後,有其他高優先順序的執行緒搶佔了CPU資源,這時此執行緒就從Running狀態變成Runnable狀態。執行緒進入Runnable狀態大體分為如下5種情況:
    -> 呼叫sleep()方法後經過的時間超過了指定的休眠時間。
    ->執行緒呼叫的阻塞IO已經返回,阻塞方法執行完畢。
    ->執行緒成功地獲得了試圖同步的監視器。
    ->執行緒正在等待某個通知,其他執行緒發出了通知。
    ->處於掛起狀態的執行緒呼叫了resume恢復方法。
  • 3 )Blocked是阻塞的意思,例如遇到了一個IO操作,此時CPU處於空閒狀態,可能會轉而把CPU時間片分配給其他執行緒,這時也可以稱為「暫停」狀態。Blocked狀態結束後,進人Runnable狀態,等待系統重新分配資源。出現阻塞的情況大體分為如下5種:
    ->執行緒呼叫sleep方法,主動放棄佔用的處理器資源。
    ->執行緒呼叫了阻塞式IO方法,在該方法返回前,該執行緒被阻塞。
    ->執行緒試圖獲得一個同步監視器,但該同步監視器正被其他執行緒所持有。
    ->執行緒等待某個通知。
    ->程式呼叫了suspend方法將該執行緒掛起。此方法容易導致死鎖,儘量避免使用該方法。
  • 4 ) run()方法執行結束後進入銷燬階段,整個執行緒執行完畢。
    每個鎖物件都有兩個佇列,一個是就緒佇列,一個是阻塞佇列。就緒佇列儲存了將要獲得鎖的執行緒,阻塞佇列儲存了被阻塞的執行緒。一個執行緒被喚醒後,才會進入就緒佇列,等待CPU的排程;反之,一個執行緒被wait後,就會進入阻塞佇列,等待下一次被喚醒。

假死現象

  • 「假死」的現象其實就是執行緒進入 WAITING等待狀態。如果全部執行緒都進人WAITING狀態,則程式就不再執行任何業務功能了,整個專案呈停止狀態。這在使用生產者與消費者模式時經常遇到。
  • 服務類
public class ServiceResources {

    private List<String> list = new ArrayList<>();

    synchronized public void push() throws InterruptedException {
        while (list.size() > 0) {
            //說明有產品
            System.out.println(Thread.currentThread().getName() + " 生產者進入睡眠");
                this.wait();
        }
        System.out.println(Thread.currentThread().getName() + " 生產");
        list.add("anything");
        this.notify();
    }

    synchronized public void take() throws InterruptedException {
        while (list.size() == 0) {
            //說明此時沒有產品
            System.out.println(Thread.currentThread().getName() + " 消費者進入睡眠");
                this.wait();

        }
        System.out.println(Thread.currentThread().getName() + " 消費");
        list.remove(0);
        this.notify();
    }

}
  • 多執行緒
public class ThreadP extends Thread {

    private ServiceResources resources;

    public ThreadP(ServiceResources resources, String name) {
        this.resources = resources;
        this.setName(name);
    }

    @Override
    public void run() {
        while (true) {
            try {
                resources.push();
            } catch (InterruptedException e) {
                break;
            }
        }

    }
}
public class ThreadC extends Thread {

    private ServiceResources resources;

    public ThreadC(ServiceResources resources, String name) {
        this.resources = resources;
        this.setName(name);
    }

    @Override
    public void run() {
        while (true) {
            try {
                resources.take();
            } catch (InterruptedException e) {
                break;
            }
        }

    }
}
  • 執行類
public class Run {

    public static void main(String[] args) throws InterruptedException {
        ServiceResources resources = new ServiceResources();
        //建立多個生產者和多個消費者
        ThreadP threadP1 = new ThreadP(resources, "P1");
        ThreadP threadP2 = new ThreadP(resources, "P2");
        ThreadC threadC1 = new ThreadC(resources, "C1");
        ThreadC threadC2 = new ThreadC(resources, "C2");
        threadP1.start();
        threadP2.start();
        threadC1.start();
        threadC2.start();
        Thread.sleep(5000);
        threadP1.interrupt();
        threadP2.interrupt();
        threadC1.interrupt();
        threadC2.interrupt();
        //獲得所有執行緒的狀態
        Thread[] threadArray = new Thread[Thread.currentThread().getThreadGroup().activeCount()];
        Thread.currentThread().getThreadGroup().enumerate(threadArray);
        for (int i = 0; i < threadArray.length; i++) {
            System.out.println(threadArray[i].getName() + " " + threadArray[i].getState());
        }
    }
}
  • 執行結果
    在這裡插入圖片描述
  • 從列印的資訊來看,呈假死狀態的程序中所有的執行緒都呈WAITING狀態。為什麼會出現這樣的情況呢?在程式碼中已經用了wait/notify啊?
  • 在程式碼中確實已經通過wait/notify進行通訊了,但不保證notify喚醒的是異類,也許是同類,比如「生產者」喚醒「生產者」,或「消費者」喚醒「消費者」這樣的情況。如果按這樣情況執行的比率積少成多,就會導致所有的執行緒都不能繼續執行下去,大家都在等待,都呈WAITING狀態,程式最後也就呈「假死」狀態,不能繼續執行下去了。

解決

  • 解決「假死」的情況很簡單,將P.java和C.java檔案中的notifyO改成notifyAll0)方法即可,它的原理就是不光通知同類執行緒,也包括異類。這樣就不至於出現假死的狀態了,程式會一直執行下去。

wait條件改變異常

  • 這個問題多見與少生產多消費中, 簡單來說就是生產趕不上消費, 導致在消費的時候發生異常
  • 服務類
public class ServiceResources {

    private List<String> list = new ArrayList<>();

    synchronized public void push() throws InterruptedException {
        if (list.size() > 0) {
            //說明有產品
            System.out.println(Thread.currentThread().getName() + " 生產者進入睡眠");
                this.wait();
        }
        System.out.println(Thread.currentThread().getName() + " 生產");
        list.add("anything");
        this.notifyAll();
    }

    synchronized public void take() throws InterruptedException {
        if (list.size() == 0) {
            //說明此時沒有產品
            System.out.println(Thread.currentThread().getName() + " 消費者進入睡眠");
                this.wait();

        }
        System.out.println(Thread.currentThread().getName() + " 消費");
        list.remove(0);
        this.notifyAll();
    }

}
  • 執行類
public class Run {

    public static void main(String[] args) throws InterruptedException {
        ServiceResources resources = new ServiceResources();
        //建立一個生產者和多個消費者
        ThreadP threadP1 = new ThreadP(resources, "P1");
        ThreadC threadC1 = new ThreadC(resources, "C1");
        ThreadC threadC2 = new ThreadC(resources, "C2");
        ThreadC threadC3 = new ThreadC(resources, "C3");
        ThreadC threadC4 = new ThreadC(resources, "C4");
        ThreadC threadC5 = new ThreadC(resources, "C5");
        threadP1.start();
        threadC1.start();
        threadC2.start();
        threadC3.start();
        threadC4.start();
        threadC5.start();
        Thread.sleep(5000);
        //獲得所有執行緒的狀態
        Thread[] threadArray = new Thread[Thread.currentThread().getThreadGroup().activeCount()];
        Thread.currentThread().getThreadGroup().enumerate(threadArray);
        for (int i = 0; i < threadArray.length; i++) {
            System.out.println(threadArray[i].getName() + " " + threadArray[i].getState());
        }
    }
}

比問題的出現就是因為在服務類中使用了if語句作為條件判斷,程式碼如下:

if (list.size() == 0) {
            //說明此時沒有產品
            System.out.println(Thread.currentThread().getName() + " 消費者進入睡眠");
                this.wait();

        }

因為條件發生改變時並沒有得到及時的響應,所以多個呈wait狀態的執行緒被喚醒,繼而執行list.remove(0)程式碼而出現異常。解決這個辦法是,將if改成while語句即可。