Java - Thread 執行緒(二) - interface Runnable

By sunwc 2023-03-23 Java

有關創建多執行緒 (Multi-threading) 的方式 - 繼承 Thread 類,可以參考我的另一篇文章 Java - Thread 執行緒(一)

除了上述說的方式創建新的執行緒;

在實際應用面,以透過實現 interface Runnable 來做是更好的方式,詳細的說明如下:

新執行緒的創建 (Multi-threading) - 實現 interface Runnable 之步驟

1.創建一個實現了 interface Runnable 的類別

2.實現類去實現 Runnable 的抽象方法: run()

3.創建實現類的物件

4.將此物件作為參數傳遞到 Thread 類的建構子(constructor)

5.通過 Thread 類的物件調用 start()

[ 註 ] 以下的例子為非thread-safety,還必須優化;本文最後會介紹兩種優化方法

例子

/**
 * 售票窗口類
 */
class Window implements Runnable {
    
    private int ticketAmount = 20;

    @Override
    public void run() {

        while (true) {

            if (ticketAmount > 0) {

                // 進行賣票操作
                System.out.println(Thread.currentThread().getName() + " : 售出票號 - " + ticketAmount);
                ticketAmount--;
            } else {
                break;
            }
        }
    }
}




/**
 * 測試類
 * @author sunwc
 * @create 2023-03-23 下午 03:01
 */
public class ImplementsRunnableTest {

    public static void main(String[] args) {

        Window window = new Window();

        Thread t1 = new Thread(window);
        t1.setName("Window 1");
        Thread t2 = new Thread(window);
        t2.setName("Window 2");
        Thread t3 = new Thread(window);
        t3.setName("Window 3");

        t1.start();
        t2.start();
        t3.start();
    }
}

演示執行緒不安全輸出結果:

Window 1 : 售出票號 - 20
Window 2 : 售出票號 - 20
Window 3 : 售出票號 - 20
Window 2 : 售出票號 - 18
Window 2 : 售出票號 - 16
Window 1 : 售出票號 - 19
Window 2 : 售出票號 - 15
Window 3 : 售出票號 - 17
Window 2 : 售出票號 - 13
Window 2 : 售出票號 - 11
Window 2 : 售出票號 - 10
Window 2 : 售出票號 - 9
Window 2 : 售出票號 - 8
Window 2 : 售出票號 - 7
Window 2 : 售出票號 - 6
Window 2 : 售出票號 - 5
Window 2 : 售出票號 - 4
Window 2 : 售出票號 - 3
Window 2 : 售出票號 - 2
Window 2 : 售出票號 - 1
Window 1 : 售出票號 - 14
Window 3 : 售出票號 - 12

創建新執行緒:繼承 Thread 類 vs. 實現 interface Runnable

開發中會優先選擇 實現 interface Runnable 方式

原因:

  1. 實現的方式沒有 類別單一繼承 的侷限性
  2. 實現的方式更適合來處理 Multi-threading 有共享資料的情況

其實原生的 Thread 類別 也是 interface Runnable 的實現類, 所以不管是子類繼承 Thread 類 或 實現類實現 interface Runnable ,相同點是: 需要 Override run(),將執行緒要執行的邏輯寫在 run() 中


上面的售票的執行緒安全問題,問題點解釋:

  1. 售票過程中,出現了重複售出同一個票號的票、超賣的情況
  2. 原因:當某個執行緒操作車票的過程中,尚未操作完成時,其它的執行緒就參與進來

如何解決執行緒安全問題:

當一個執行緒A在操作ticket的時候,其它執行緒不能參與進來。直到執行緒A操作完ticket時,其它執行緒才可以開始操作ticket。這種情況即使執行A被阻塞了,其它執行緒也一定要等待

現實例子:

當某A在使用唯一一間廁所時,某B即使肚子痛要使用,也要等某A使用完才可以進入。因為某A鎖門了,某B只好等了

  • 在Java中,我們通過同步機制,來解決執行緒安全的問題

優化方式一、同步程式區塊

synchronized(同步鎖) {
    // 需要被同步的程式
}

說明:

  1. 共享資料的程式用synchronized(同步鎖){}包起來,當一個thread先進入這層,其它thread要先在這層外面等

  2. 共享資料:指的是Multi-threading中共同操作的一個變數,以例子來說 ticketAmount 就是共享的資料

  3. 什麼是同步鎖?

    任何一個類別的物件都可以充當鎖,但是Multi-threading 中的每個 thread 進入這層時都必須使用同一把鎖

  4. 侷限性:在同步程式區塊內,只能有一個執行緒參與,其它執行緒等待。區塊內相當於是一個單執行緒的過程,效率會比較低

例子

優化後(一)的售票窗口類:

/**
* 售票窗口類
*/
class Window implements Runnable {

    private int ticketAmount = 20;


    @Override
    public void run() {

        while(true) {

            // 同步程式區塊 - Window.class(當前類別)充當唯一一把鎖
            // 因為類別只會加載一次,當作唯一鎖相對安全
            // 用this的話要看這個類別在主執行緒(main方法)中new了幾個物件
            // 若只有一個就可以用this當唯一鎖 
            synchronized (Window.class) { // 或 synchronized (this)

                if (ticketAmount > 0) {

                    // 執行緒阻塞,提高執行緒不安全機率
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    // 進行賣票操作
                    System.out.println(Thread.currentThread().getName() + " : 售出票號 - " + ticketAmount);
                    ticketAmount--;
                } else {
                    break;
                }
            }

        }
    }
}

解決執行緒不安全輸出結果:

Window 1 : 售出票號 - 20
Window 1 : 售出票號 - 19
Window 1 : 售出票號 - 18
Window 1 : 售出票號 - 17
Window 1 : 售出票號 - 16
Window 1 : 售出票號 - 15
Window 1 : 售出票號 - 14
Window 3 : 售出票號 - 13
Window 2 : 售出票號 - 12
Window 2 : 售出票號 - 11
Window 2 : 售出票號 - 10
Window 2 : 售出票號 - 9
Window 2 : 售出票號 - 8
Window 2 : 售出票號 - 7
Window 2 : 售出票號 - 6
Window 3 : 售出票號 - 5
Window 3 : 售出票號 - 4
Window 1 : 售出票號 - 3
Window 1 : 售出票號 - 2
Window 1 : 售出票號 - 1

優化方式二、同步方法

若使用到共享資料的程式完整的寫在一個方法中,我們不妨將此方法宣告成同步的

  1. 同步方法仍然涉及到同步鎖,只是不需要我們顯示的宣告

  2. 同步鎖 :

    非靜態的同步方法:this (當前物件本身)

    靜態的同步方法:當前類別本身

例子

優化後(二)的售票窗口類:

/**
 * 售票窗口類
 */
class Window implements Runnable {

    private static int ticketAmount = 20;


    @Override
    public void run() {

        while (ticketAmount > 0) {

            // 售票
            sellTicket();
        }
    }

//    private synchronized void sellTicket() {// 默認使用this當作鎖
    private static synchronized void sellTicket() { // 默認使用Window.class當鎖

        if (ticketAmount > 0) {

            // 執行緒阻塞,提高執行緒不安全機率
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 進行賣票操作
            System.out.println(Thread.currentThread().getName() + " : 售出票號 - " + ticketAmount);
            ticketAmount--;
        }
    }

}

解決執行緒不安全輸出結果:

Window 1 : 售出票號 - 20
Window 1 : 售出票號 - 19
Window 1 : 售出票號 - 18
Window 3 : 售出票號 - 17
Window 3 : 售出票號 - 16
Window 3 : 售出票號 - 15
Window 3 : 售出票號 - 14
Window 3 : 售出票號 - 13
Window 3 : 售出票號 - 12
Window 3 : 售出票號 - 11
Window 3 : 售出票號 - 10
Window 3 : 售出票號 - 9
Window 3 : 售出票號 - 8
Window 3 : 售出票號 - 7
Window 2 : 售出票號 - 6
Window 2 : 售出票號 - 5
Window 2 : 售出票號 - 4
Window 2 : 售出票號 - 3
Window 2 : 售出票號 - 2
Window 2 : 售出票號 - 1

總結

本文幫助了解如何

1.透過實現類 implements interface Runnable 達成 Multi-threading

2.透過將共享資源宣告成「同步的」以解決執行緒不安全問題

延伸學習:有關透過鎖 Lock 解決執行緒不安全問題,可以參考我的另一篇文章 Java - Lock - 解決執行緒不安全