Java多執行緒的實現方式

2020-07-16 10:04:38
在 Java 的 JDK 開發包中,已經自帶了對多執行緒技術的支援,可以方便地進行多執行緒程式設計。實現多執行緒程式設計的方式主要有兩種:一種是繼承 Thread 類,另一種是實現 Runnable 介面。下面詳細介紹這兩種具體實現方式。

繼承 Thread 類

在學習如何實現多執行緒前,先來看看 Thread 類的結構,如下:
public class Thread implements Runnable

從上面的原始碼可以發現,Thread 類實現了 Runnable 介面,它們之間具有多型關係。

其實,使用繼承 Thread 類的方式實現多執行緒,最大的局限就是不支援多繼承,因為 Java 語言的特點就是單根繼承,所以為了支援多繼承,完全可以實現 Runnable 介面的方式,一邊實現一邊繼承。但用這兩種方式建立的執行緒在工作時的性質是一樣的,沒有本質的區別。

Thread 類有如下兩個常用構造方法:
  1. public Thread(String threadName)
  2. public Thread()

繼承 Thread 類實現執行緒的語法格式如下:
public class NewThreadName extends Thread
{    //NewThreadName 類繼承自 Thread 類
    public void run()
    {
        //執行緒的執行程式碼在這裡
    }
}

執行緒實現的業務程式碼需要放到 run() 方法中。當一個類繼承 Thread 類後,就可以在該類中覆蓋 run() 方法,將實現執行緒功能的程式碼寫入 run() 方法中,然後同時呼叫 Thread 類的 start() 方法執行執行緒,也就是呼叫 run() 方法。

Thread 物件需要一個任務來執行,任務是指執行緒在啟動時執行的工作,該工作的功能程式碼被寫在 run() 方法中。當執行一個執行緒程式時,就會自動產生一個執行緒,主方法正是在這個執行緒上執行的。當不再啟動其他執行緒時,該程式就為單執行緒程式。主方法執行緒啟動由 Java 虛擬機器負責,開發人員負責啟動自己的執行緒。

如下程式碼演示了如何啟動一個執行緒:
new NewThreadName().start();    //NewThreadName 為繼承自 Thread 的子類

注意:如果 start() 方法呼叫一個已經啟動的執行緒,系統將會丟擲 IllegalThreadStateException 異常。

例 1

編寫一個 Java 程式演示執行緒的基本使用方法。這裡建立的自定義執行緒類為 MyThread,此類繼承自 Thread,並在重寫的 run() 中輸出一行字串。

MyThread 類程式碼如下:
public class MyThread extends Thread
{
    @Override
    public void run()
    {
        super.run();
        System.out.println("這是執行緒類 MyThread");
    }
}

接下來編寫啟動 MyThread 執行緒的主方法,程式碼如下:
public static void main(String[] args)
{
    MyThread mythread=new MyThread();    //建立一個執行緒類
    mythread.start();    //開啟執行緒
    System.out.println("執行結束!");    //在主執行緒中輸出一個字串
}

執行上面的程式將看到如下所示的執行效果。
執行結束!
這是執行緒類 MyThread

從上面的執行結果來看,MyThread 類中 run() 方法執行的時間要比主執行緒晚。這也說明在使用多執行緒技術時,程式碼的執行結果與程式碼執行順序或呼叫順序是無關的。同時也驗證了執行緒是一個子任務,CPU 以不確定的方式,或者說以隨機的時間來呼叫執行緒中的 run() 方法,所以就會出現先列印“執行結束!”,後輸出“這是執行緒類MyThread”這樣的結果了。

例 2

上面介紹了執行緒的呼叫具有隨機性,為了更好地理解這種隨機性這裡編寫了一個案例進行演示。

(1) 首先建立自定義的執行緒類 MyThread01,程式碼如下:
package ch14;
public class MyThread01 extends Thread
{
    @Override 
    public void run()
    { 
        try
        { 
            for(int i=0;i<10;i++)
            { 
                int time=(int)(Math.random()*1000); 
                Thread.sleep(time); 
                System.out.println("當前執行緒名稱="+Thread.currentThread().getName()); 
            } 
        }
        catch(InterruptedException e)
        { 
            e.printStackTrace(); 
        } 
    } 
}

(2) 接下來編寫主執行緒程式碼,在這裡除了啟動上面的 MyThread01 執行緒外,還實現了 MyThread01 執行緒相同的功能。主執行緒的程式碼如下:
package ch14;
public class Test02
{
    public static void main(String[] args)
    { 
        try
        { 
            MyThread01 thread=new MyThread01(); 
            thread.setName("myThread"); 
            thread.start(); 
            for (int i=0;i<10;i++)
            { 
                int time=(int)(Math.random()*1000); 
                Thread.sleep(time); 
                System.out.println("主執行緒名稱="+Thread.currentThread().getName()); 
            } 
        }
        catch(InterruptedException e)
        { 
            e.printStackTrace(); 
        }
    }
}

在上述程式碼中,為了展現出執行緒具有隨機特性,所以使用亂數的形式來使執行緒得到掛起的效果,從而表現出 CPU 執行哪個執行緒具有不確定性。

MyThread01 類中的 start() 方法通知“執行緒規劃器”此執行緒已經準備就緒,等待呼叫執行緒物件的 run() 方法。這個過程其實就是讓系統安排一個時間來呼叫 Thread 中的 run() 方法,也就是使執行緒得到執行,啟動執行緒,具有非同步執行的效果。

如果呼叫程式碼 thread.run() 就不是非同步執行了,而是同步,那麼此執行緒物件並不交給“執行緒規劃器”來進行處理,而是由 main 主執行緒來呼叫 run() 方法,也就是必須等 run() 方法中的程式碼執行完後才可以執行後面的程式碼。

這種採用亂數延時呼叫執行緒的方法又稱為非同步呼叫,程式執行的效果如下所示。
當前執行緒名稱=myThread
主執行緒名稱=main
當前執行緒名稱=myThread
當前執行緒名稱=myThread
當前執行緒名稱=myThread
主執行緒名稱=main
當前執行緒名稱=myThread
當前執行緒名稱=myThread
主執行緒名稱=main
當前執行緒名稱=myThread
主執行緒名稱=main
當前執行緒名稱=myThread
當前執行緒名稱=myThread
當前執行緒名稱=myThread
主執行緒名稱=main
主執行緒名稱=main
主執行緒名稱=main
主執行緒名稱=main
主執行緒名稱=main
主執行緒名稱=main

例 3

除了非同步呼叫之外,同步執行執行緒 start() 方法的順序不代表執行緒啟動的順序。下面建立一個案例演示同步執行緒的呼叫。

(1) 首先建立自定義的執行緒類 MyThread02,程式碼如下:
package ch14;
public class MyThread02 extends Thread
{
    private int i; 
    public MyThread02(int i)
    { 
        super(); 
        this.i=i; 
    } 
    @Override 
    public void run()
    { 
        System.out.println("當前數位:"+i); 
    }
}

(2) 接下來編寫主執行緒程式碼,在這裡建立 10 個執行緒類 MyThread02,並按順序依次呼叫它們的 start() 方法。主執行緒的程式碼如下:
package ch14;
public class Test03
{
    public static void main(String[] args)
    { 
        MyThread02 t11=new MyThread02(1); 
        MyThread02 t12=new MyThread02(2); 
        MyThread02 t13=new MyThread02(3); 
        MyThread02 t14=new MyThread02(4); 
        MyThread02 t15=new MyThread02(5); 
        MyThread02 t16=new MyThread02(6); 
        MyThread02 t17=new MyThread02(7); 
        MyThread02 t18=new MyThread02(8); 
        MyThread02 t19=new MyThread02(9); 
        MyThread02 t110=new MyThread02(10); 
        t11.start(); 
        t12.start(); 
        t13.start(); 
        t14.start(); 
        t15.start(); 
        t16.start(); 
        t17.start(); 
        t18.start(); 
        t19.start(); 
        t110.start(); 
    }
}

程式執行後的結果如下所示,從執行結果中可以看到,雖然呼叫時數位是有序的,但是由於執行緒執行的隨機性,導致輸出的數位是無序的,而且每次順序都不一樣。
當前數位:1
當前數位:3
當前數位:5
當前數位:7
當前數位:6
當前數位:2
當前數位:4
當前數位:8
當前數位:10
當前數位:9

實現 Runnable 介面

如果要建立的執行緒類已經有一個父類別,這時就不能再繼承 Thread 類,因為 Java 不支援多繼承,所以需要實現 Runnable 介面來應對這樣的情況。

實現 Runnable 介面的語法格式如下:
public class thread extends Object implements Runnable

提示:從 JDK 的 API 中可以發現,實質上 Thread 類實現了 Runnable 介面,其中的 run() 方法正是對 Runnable 介面中 run() 方法的具體實現。

實現 Runnable 介面的程式會建立一個 Thread 物件,並將 Runnable 物件與 Thread 物件相關聯。Thread 類有如下兩個與 Runnable 有關的構造方法:
  1. public Thread(Runnable r);
  2. public Thread(Runnable r,String name);

使用上述兩種構造方法之一均可以將 Runnable 物件與 Thread 範例相關聯。使用 Runnable 介面啟動執行緒的基本步驟如下。
  1. 建立一個 Runnable 物件。
  2. 使用引數帶 Runnable 物件的構造方法建立 Thread 範例。
  3. 呼叫 start() 方法啟動執行緒。

通過實現 Runnable 介面建立執行緒時開發人員首先需要編寫一個實現 Runnable 介面的類,然後範例化該類的物件,這樣就建立了 Runnable 物件。接下來使用相應的構造方法建立 Thread 範例。最後使用該範例呼叫 Thread 類的 start() 方法啟動執行緒,如圖 1 所示。


圖1 使用Runnable介面啟動執行緒流程