day32-執行緒基礎02

2022-09-04 21:03:41

執行緒基礎02

3.繼承Thread和實現Runnable的區別

  1. 從java的設計來看,通過繼承Thread或者實現Runnable介面本身來建立執行緒本質上沒有區別,從jdk幫助檔案我們可以看到Thread類本身就實現了Runnable介面
  2. 實現Runnable介面方式更加適合多個執行緒共用一個資源的情況,並且避免了單繼承的限制,建議使用Runnable介面
image-20220904180958493

3.1多執行緒售票問題

程式設計模擬三個售票視窗售票100張,分別使用繼承Thread類和實現Runnable介面的方法,並分析有什麼問題?

1.使用繼承Thread的方法:

package li.thread;

//使用多執行緒,模擬三個視窗同時售票共100張
public class SellTicket {
    public static void main(String[] args) {

        SellTicket01 sellTicket01 = new SellTicket01();
        SellTicket01 sellTicket02 = new SellTicket01();
        SellTicket01 sellTicket03 = new SellTicket01();

        sellTicket01.start();//啟動售票執行緒
        sellTicket02.start();//啟動售票執行緒
        sellTicket03.start();//啟動售票執行緒
    }
}

//1.使用繼承Thread類的方式
class SellTicket01 extends Thread {

    //多個物件共用同一個靜態成員變數(多個範例的static變數會共用同一塊記憶體區域)
    private static int ticketNum = 100;//讓多個執行緒共用ticketNum

    @Override
    public void run() {
        while (true) {

            if (ticketNum <= 0) {
                System.out.println("售票結束...");
                break;
            }

            //休眠50毫秒,模擬
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("視窗:" + Thread.currentThread().getName() + "售出一張票 "
                    + "剩餘票數:" + (--ticketNum));
        }
    }
}
image-20220904183332164

一個顯然的問題是,剩餘票數竟然是負數!

原因是:每個執行緒都要進行票數判斷才能進行下一步操作,假設某時刻票數還剩2張,此時執行緒0判斷條件ticketNum <= 0不成立;於此同時,執行緒1執行緒2也同時進行了判斷,三者都通過了判斷,於是都認為此刻票數為2,都進行-1售票操作。於是三者結束後就會出現總票數為-1 的情況。

可以看到,造成票數超賣的主要原因是三個執行緒同時操作一個資源。

2.使用實現介面Runnable的方式:

package li.thread;

//使用多執行緒,模擬三個視窗同時售票共100張
public class SellTicket {
    public static void main(String[] args) {

        SellTicket02 sellTicket02 = new SellTicket02();
        new Thread(sellTicket02).start();//第1個執行緒-視窗
        new Thread(sellTicket02).start();//第2個執行緒-視窗
        new Thread(sellTicket02).start();//第3個執行緒-視窗
    }
}

class SellTicket02 implements Runnable {

    private int ticketNum = 100;

    @Override
    public void run() {
        while (true) {

            if (ticketNum <= 0) {
                System.out.println("售票結束...");
                break;
            }

            //休眠50毫秒,模擬
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("視窗:" + Thread.currentThread().getName() + "售出一張票 "
                    + "剩餘票數:" + (--ticketNum));
        }
    }

}
image-20220904185119423

可以看到,實現介面Runnable的方式同樣發生了票數為負數的情況,原因與上面一致,是由於多個執行緒同時操作一個資源而造成的。

要解決類似的問題,就要引入執行緒的同步和互斥的概念。該問題將在之後解決。

4.執行緒終止

  • 基本說明:
  1. 當執行緒完成任務後,會自動退出
  2. 還可以通過使用變數來控制run方法退出的方式來停止執行緒,即通知方式

例子:

啟動一個執行緒t,要求在main執行緒中去停止執行緒t,請程式設計實現。

package li.thread.exit_;

public class ThreadExit_ {
    public static void main(String[] args) throws InterruptedException {
        T t = new T();
        t.start();

        //如果希望main執行緒可以去控制 t1執行緒的終止,必須可以修改loop
        //讓 t1退出run方法,從而終止 t1執行緒 -->稱為 通知方式

        //讓主執行緒休眠 10秒,在通知 t1執行緒退出
        System.out.println("主執行緒休眠10秒...");
        Thread.sleep(10*1000);

        t.setLoop(false);
    }
}

class T extends Thread {
    int count = 0;

    //設定一個控制變數
    private boolean loop = true;

    @Override
    public void run() {
        while (loop) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("T 執行中..."+(++count));
        }
    }

    public void setLoop(boolean loop) {
        this.loop = loop;
    }
}
image-20220904192155687

可以用於一個執行緒通過變數控制另一個執行緒終止的情況。

5.執行緒常用方法

  • 常用方法第一組:
  1. setName //設定執行緒名稱,使之與引數name相同
  2. getName //返回該執行緒的名稱
  3. start //使該執行緒開始執行;Java虛擬機器器底層呼叫該執行緒的start0()方法
  4. run //呼叫執行緒物件run方法
  5. setPriority //更改執行緒的優先順序
  6. getPriority // 獲取執行緒的優先順序
  7. sleep //在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行)
  8. interrupt //中斷執行緒

注意事項和細節:

  • start方法底層會建立新的執行緒,呼叫run,run就是一個簡單的方法呼叫,不會啟動新的執行緒
  • 執行緒優先順序的範圍
image-20220904194308729
  • interrupt,中斷執行緒,但並沒有真正地結束執行緒。所以一般用於中斷正在休眠的執行緒
  • sleep:執行緒的靜態方法,使當前執行緒休眠

例子1:

package li.thread.method;

public class ThreadMethod01 {
    public static void main(String[] args) throws InterruptedException {
        //測試相關方法
        T t = new T();
        t.setName("jack");//設定執行緒的名稱
        t.setPriority(Thread.MIN_PRIORITY);
        t.start();//啟動子執行緒

        //主執行緒列印5句hi,然後中斷子執行緒的休眠
        for (int i = 0; i < 5; i++) {
            Thread.sleep(1000);
            System.out.println("hi" + i);
        }

        System.out.println(t.getName() + "執行緒的優先順序=" + t.getPriority());
        t.interrupt();//當執行到這裡的時候,就會中斷 t執行緒的休眠
    }
}

class T extends Thread {//自定義的執行緒類

    @Override
    public void run() {
        while (true) {//每隔5秒吃100個包子,然後休眠5秒,再吃...
            for (int i = 0; i < 100; i++) {
                //Thread.currentThread().getName()獲取當前執行緒的名稱
                System.out.println(Thread.currentThread().getName() + "吃包子~~~" + i);
            }
            try {
                System.out.println(Thread.currentThread().getName() + "休眠中~~~");
                sleep(20000);//休眠20秒
            } catch (InterruptedException e) {
                //當該執行緒執行到一個interrupt方法時,就會catch一個異常,可以加入自己的業務程式碼
                //InterruptedException是捕獲到一箇中斷異常
                System.out.println(Thread.currentThread().getName() + "被interrupt了");
            }
        }
    }
}
image-20220904200420750
  • 常用方法第二組:
  1. yield:執行緒的禮讓。讓出cpu,讓其他執行緒執行,但禮讓的時間不確定,所以也不一定禮讓成功。

  2. join:執行緒的插隊。插隊的執行緒一旦插隊成功,則肯定先執行完插入的執行緒的所有任務

案例:建立一個子執行緒,每個1秒輸出hello,輸出20次;主執行緒每隔1秒輸出hi,輸出20次。要求:兩個執行緒同時執行,當主執行緒輸出5次後,就讓子執行緒執行完畢,主執行緒再繼續。