天天看點

多線程1(線程的建立和啟動)

    Java 使用Thread 類來代表線程,所有的線程對象都必須是Thread 或其子類的執行個體。每個線程的作用是完成一定的任務,實際上就是執行一段程式流(一段順尋執行的代碼)。Java 使用線程執行體來代表這段程式流。     1、繼承Thread 類建立線程類     通過繼承Thread 類來建立并啟動多線程的步驟如下:     ① 定義Thread 類的子類,并建立該類的run()方法,該run()方法的方法體就代表了線程需要完成的任務。是以把run()方法稱為線程執行體。     ② 建立Thread 子類的執行個體,即建立了線程對象。     ③ 調用線程對象的start() 方法來啟動該線程。

    下面程式示範了通過繼承Thread 類來建立并啟動多線程。

// 通過繼承Thread 類來建立線程類
public class FirstThread extends Thread {

    private int i;
    // 重寫run() 方法,run() 方法的方法體就是線程執行體
    public void run(){
        for(; i < 100; i++){
            // 當線程類繼承Thread 類時,直接使用this即可擷取目前線程
            // Thread 對象的getName()傳回目前線程的名字
            // 是以可以直接調用getName() 方法傳回目前線程的名字
            System.out.println(getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for(int i = 0; i < 100; i++){
            // 調用Thread 的currentThread()方法擷取目前線程
            System.out.println(Thread.currentThread().getName()
                    + " " + i);
            if(i == 20){
                // 建立并啟動第一個線程
                new FirstThread().start();
                // 建立并啟動第二個線程
                new FirstThread().start();
            }
        }
    }

}
           

運作結果如下:

多線程1(線程的建立和啟動)
多線程1(線程的建立和啟動)

    上面程式中的FirstThread 類繼承了Thread類,并實作了run()方法,該run()方法裡的代碼執行流就是該線程所需要完成的任務。程式的主方法中包含了一個循環,當循環變量i等于20時建立并啟動兩個新線程。運作後會得到上面的界面。     雖然上面程式隻顯示地建立了2個線程,但實際上程式有三個線程,即程式顯示建立的2個子線程和主線程。當Java 程式開始運作後,程式至少會建立一個主線程,主線程的線程執行體不是由run()方法确的,而是由main()方法确定的——main()方法的方法體代表主線程的線程執行體。     注:使用繼承Thread 類的方法來建立線程類時,多個線程之間無法共享線程的執行個體變量。

    2、實作Runnable接口建立線程類     實作Runnable接口來建立并啟動多個線程的步驟:     ① 定義Runnable接口的實作類,并重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。     ② 建立Runnable實作類的執行個體,并以此執行個體作為Thread的target 來建立Thread對象,該Thread對象才是真正的線程對象。代碼如下所示:     

// 建立Runnable實作類的對象
    SecondThread st = new SecondThread();
    // 以Runnable實作類的對象作為Thread的target來建立Thread對象,即線程對象。
    new Thread(st);
    也可以在建立Thread對象時為該Thread對象指定一個名字,代碼如下所示:
    // 建立Thread對象時指定target和新線程的名字
    new Thread(st, "新線程1");
           

    注意:Runnable對象僅僅作為Thread對象的target,Runnable實作類裡包含的run()方法僅作為線程執行體。而實際的線程對象依然是Thread執行個體,隻是該Thread線程負責執行其target的run()方法。

    ③ 調用線程對象的start()方法來啟動線程。

下面程式示範了通過實作Runnable接口來建立并啟動多線程。

// 通過實作Runnable接口來建立線程類
public class SecondThread implements Runnable {

    private int i;
    // run()方法同樣是線程執行體
    public void run() {
        for(; i < 100; i++){
            // 當線程實作Runnable接口時
            // 如果想擷取目前線程,隻能用Thread.currentThread()方法
            System.out.println(Thread.currentThread().getName()
                    + " " + i);
        }
    }

    public static void main(String[] args) {
        for(int i = 0; i < 100; i++){
            System.out.println(Thread.currentThread().getName()
                    + " " + i);
            if(i == 20){
                SecondThread st = new SecondThread();
                // 通過new Thread(target, name)方法建立新線程
                new Thread(st, "新線程1").start();
                new Thread(st, "新線程2").start();
            }
        }
    }

}
           

運作結果如下:

多線程1(線程的建立和啟動)

    上面程式中的run()方法,定義了該線程的線程執行體。對比FirstThread中的run()方法和SecondThread中的run()方法體不難發現,通過繼承Thread類來獲得目前線程對象比較簡單,直接使用this就可以了;但通過實作Runnable接口擷取目前線程對象,則必須使用Thread.currentThread()方法。     除此之外,上面程式建立了兩個Thread對象,并調用start()方法來啟動着兩個線程。在FirstThread和SecondThread中建立線程對象的方式有所差別:前者直接建立的Thread子類即可代表線程對象;後者建立的Runnable對象隻能作為線程對象的target。     從運作結果來看,兩個子線程的i變量是連續的,也就是采用Runnable接口的方式建立的多個線程可以共享線程類的執行個體變量。這是因為在這種方式下,程式所建立的Runnable對象隻是線程的target,而多個線程可以共享一個target,是以多個線程可以共享同一個線程類(實際上應該是線程的target類)。

3、使用Callable和Future建立線程     從Java 5 開始,Java提供了Callable接口,該接口提供了一個call()方法可以作為線程執行體,但call()方法比run()方法功能更強大。

  • call()方法可以有傳回值。
  • call()方法可以聲明抛出異常。

    是以完全可以提供一個Callable對象作為Thread的target,而該線程的線程執行體就是該Callable對象的call()方法。問題是:Callable接口是Java 5 新增的接口,而且它不是Runnable接口的子接口,是以Callable對象不能直接作為Thread的target。而且call()方法還有一個傳回值——call()方法并不能直接調用,它是作為線程執行體被調用的。那麼如何擷取call()方法的傳回值呢?     Java 5 提供了Future 接口來代表Callable接口裡call()方法的傳回值,并為Future接口提供了一個FutureTask實作類,該實作類實作了Future接口,并實作了Runnable接口——可以作為Thread類的target。     在Future接口裡定義了如下幾個公共方法來控制它關聯的Callable任務。

  • boolean cancel(boolean mayInterrupIfRunning): 試圖取消該Future裡關聯的Callable任務。
  • V get(long timeout, TimeUnit unit): 傳回Callable任務裡call()方法的傳回值。該方法讓程式最多阻塞timeout和unit指定的時間,如果經過指定時間後Callable任務依然沒有傳回值,将會抛出TimeoutException異常。
  • boolean isCancelled(): 如果在Callable任務正常完成前被取消。則傳回true。
  • boolean isDone(): 如果Callable任務已完成,則傳回true。 

    建立并啟動有傳回值的線程的步驟如下:     ① 建立Callable接口的實作類,并實作call()方法,該call()方法将作為線程執行體,且該call()方法有傳回值,在建立Callable實作類的執行個體。     ② 使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的傳回值。     ③ 使用FutureTask對象作為Thread對象的target建立并啟動新線程。     ④ 調用FutureTask對象的get()方法來獲得子線程執行結束後的傳回值。

下面程式通過實作Callable()接口來實作線程類,并啟動該線程。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThirdThread {

    public static void main(String[] args) {
        // 建立Callable對象
        ThirdThread rt = new ThirdThread();
        // 先使用Lambda表達式建立Callable<Integer>對象
        // 使用FutureTask來包裝Callable對象
        FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
            int i = 0;
            for(; i < 100; i++){
                System.out.println(Thread.currentThread().getName()
                        + " 的循環變量i的值: " + i);
            }
            // call()方法可以有傳回值
            return i;
        });
        for(int i = 0; i < 100; i++){
            System.out.println(Thread.currentThread().getName()
                    + " 的循環變量i的值: " + i);
            if(i == 20){
                // 實質還是以Callable對象來建立并啟動線程的
                new Thread(task, "有傳回值的線程").start();
            }
        }

        try {
            System.out.println("子線程的傳回值:" + task.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
           

    上面程式中的粗體字代碼是以Callable對象來啟動線程的關鍵代碼。程式先使用Lambda表達式建立一個Callable對象,然後将該執行個體包裝成一個FutureTask對象。主線程中當線程變量i等于20時,程式啟動以FutureTask對象為target的線程。程式最後調用FutureTask對象的get()方法來傳回call()方法的傳回值——該方法将導緻主線程被阻塞,知道call()方法結束并傳回為止。     在運作程式結果中,将看到主線程和call()方法所代表的線程交替執行的情形,程式最後還會輸出call()方法的傳回值。

4、建立線程的三種方式對比     通過繼承Thread類或實作Runnable、Callable接口都可以實作多線程,不過實作Runnable接口與實作callable接口的方式基本相同,隻是Callable接口裡定義的方法有傳回值,可以聲明抛出異常而已。是以可以将實作Runnable接口和實作Callable接口歸并為一種方式。這種方式與繼承Thread方式之間的主要差别如下。     采用實作Runnable、Callable接口的方式建立多線程的優缺點:

  • 線程類隻是實作了Runnable接口或Callable接口,還可以繼承其他類。
  • 在這種情況下,多個線程可以共享同一個target對象,是以非常适合多個相同線程來處理同一份資源的情況,進而可以将CPU、代碼和資料分開,形成清晰的模型,較好地展現了面向對象的思想。
  • 劣勢是,變成稍稍複雜,如果需要通路目前線程,則必須使用Thread.currentThread()方法。

    采用繼承Thread類的方式建立多線程的優缺點:

  • 劣勢是,因為線程類已經繼承了Thread,是以不能在繼承其他父類。
  • 優勢是變成簡單,如果需要通路目前線程,則無須使用Thread.currentThread()方法,直接使用this即可獲得目前線程。

    鑒于上面分析,是以一般推薦采用實作Runnable接口,Callable接口的方式來建立線程。

上一篇: 信号槽