【併發那些事 】創建線程的三種方式

創建線程可以說是併發知識中最基礎的操作了,JDK 提供的創建線程的方式,如果不包括通過線程池的話,目前有三種形式,它們分別是通過繼承 Thread 類,通過實現 Runable 接口,通過 FutureTask。如下圖所示

下面整理了一下 3 種方法的具體使用與異同。

創建線程的 3 種方法

1. 繼承 Thread

  1. 創建一個類繼承 Thread 並覆蓋 run 方法
class MyThread extends Thread {
    @Override
    public void run() {
        String threadName = getName();
        for (int i = 0; i < 20; i++) {
            System.out.println("線程[" + threadName + "]運行開始,i = " + i + " time = " + new Date());
        }
    }
}
  1. 創建並啓動線程
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();

        myThread1.start();
        myThread2.start();

        String threadName = Thread.currentThread().getName();
        for (int i = 0; i < 20; i++) {
            System.out.println("線程[" + threadName + "]運行開始,i = " + i + " time = " + new Date());
        }

整體流程如下:

這裏步驟比較簡單和清晰

  1. 創建一個類,這類要繼承 Thread
  2. 覆蓋 Thread 的 run 方法,並在此方法中實現你的多線程任務
  3. 創建這個類的實例
  4. 調用它的 start() 方法(這裏要注意,新手容易直接調用 run 方法,那樣只是普通調用,而不多線程調用)

2. 實現 Runable

  1. 創建一個類實現 Runable 接口,並覆蓋 run 方法
class MyRunable implements Runnable {
    
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        for (int i = 0; i < 20; i++) {
            System.out.println("線程[" + threadName + "]運行開始,i = " + i + " time = " + new Date());
        }
    }
}
  1. 創建類的實現,並將它傳給 Thread 的構造函數來創建 Thread 
MyRunable myRunable = new MyRunable();

new Thread(myRunable).start();
new Thread(myRunable).start();

String threadName = Thread.currentThread().getName();
for (int i = 0; i < 20; i++) {
    System.out.println("線程[" + threadName + "]運行開始,i = " + i + " time = " + new Date());
}

整體流程如下:

具體步驟如下:

  1. 創建一個實現了 Runable 接口的類
  2. 覆蓋 run 方法,並在此方法中實現你的多線程任務
  3. 創建 Runable 接口實現類的實例
  4. 通過把上步得到的 Runable 接口實現類的實例,傳給  Thread 的有參構造函數來創建 Thread 的實例
  5. 調用上步得來的 Thread 實例的 start() 方法(這裏要注意,新手容易直接調用 run 方法,那樣只是普通調用,而不多線程調用)

3. 通過 FutureTask

  1. 創建一個實現了 Callable 接口的類,並實現call 方法 (T 代表你想要的返回值類型)
class MyCallerTask implements Callable<String> {
    
    @Override
    public String call() throws Exception {
        System.out.println("執行任務開始");
        Thread.sleep(3000);
        System.out.println("執行任務結束");
        return "hello";
    }
}
  1. 創建並啓動線程
        // 創建異步任務
        FutureTask<String> futureTask = new FutureTask<>(new MyCallerTask());
        // 啓動線程
        new Thread(futureTask).start();
        System.out.println("其它操作");
        try {
            // 等待任務執行完,並獲得任務執行完的結果
            String result = futureTask.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

整體流程如下:

具體步驟如下:

  1. 創建一個實現了 Callable 接口的類,這裏 T 的類型就是你線程任務想要返回的類型
  2. 覆蓋 call 方法,並在此方法中實現你的多線程任務
  3. 創建 Callable 接口實現類的實例
  4. 通過把上步得到的 Callable 接口實現類的實例,傳給  FutureTask 的有參構造函數來創建 FutureTask 的實例
  5. 通過把上步得到的 FutureTask 實例,傳給  Thread 的有參構造函數來創建 Thread 的實例
  6. 調用上步得來的 Thread 實例的 start() 方法(這裏要注意,新手容易直接調用 run 方法,那樣只是普通調用,而不多線程調用)
  7. 如果你還想獲得返回值,需要再調用 FutureTask 實例的 get() 方法(這裏要注意,get()會阻塞線程)

3種方法的優缺點

通過上述的演示代碼,可以看出這 3 種方法,其實各有優缺點

複雜程度

通過代碼量與邏輯可以明顯感覺出來,第一種直接繼承 Thread 最方便,並且其它兩種到最後,還是要依賴創建 Thread 才能實現。所以從方便及難易程度來看,可以得到如下結論:

可擴展性

通過演示代碼可以看出,只有第一種是通過繼承,其它兩種是通過實現接口的形式。我們都知道 JAVA 是不允許多繼承,但是可以多實現。所以如果使用了第一種方法,就無法再繼承別的類了。另外第一種把線程與線程任務冗餘在了一起,不利於後期的維護。所以可以得到如下結論:

是否有返回值

從代碼中可以很容易看出,只有通過 FutureTask 的方式纔有返回值,另外兩種均沒有,所以得出如下結論

該用哪種方式創建線程

如果要用到返回值,那不用想,肯定只能使用 FutureTask 的方法。如果對於返回值沒有要求,那Thread 與  Runable 均可,不過,考慮到可擴展性,最好使用 Runable 的形式。不過,話說回來,如果在真正項目中使用,綜合考慮,一般還是最推薦通過線程池去創建。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章